factoriax.engine.recipes#

Recipe definitions for the Factoriax environment.

Recipe data sits in three layers:

  • Recipe is one record. It holds the item that the recipe produces, the items that it consumes, and the time that it takes.

  • RecipeBook is a validated tuple of records. It enforces the rules that the recipe matcher of the engine depends on.

  • RecipeTable turns a book into stacked JAX arrays, which is what JIT-compiled engine code reads.

BASE_RECIPES is the tuple that comes with the engine, and DEFAULT_RECIPE_TABLE is its projection. A scenario can supply its own book, so engine code takes the size of its loops from the table that it receives, and not from NUM_RECIPES.

A recipe runs on a furnace or on an assembler. The output item decides which one, and no recipe declares a machine. A recipe takes one or two input types. The projection pads an unused slot with (EMPTY, 0), so every row is a two-slot lookup.

A player who crafts a recipe pays from the inventory and gets the output in the same step. An assembler or a furnace takes ticks steps instead. Nothing can write over its output slot, so the rate at which something withdraws that output limits its throughput.

Module Attributes

MAX_RECIPE_INPUTS

Largest number of different input item types in one recipe.

BASE_RECIPES

The recipes that come with the engine, in the order that gives them their ids.

NUM_RECIPES

Number of recipes in BASE_RECIPES.

RECIPE_NAMES

Names of BASE_RECIPES.

BASE_RECIPE_BOOK

The RecipeBook that comes with the engine.

DEFAULT_RECIPE_TABLE

Default RecipeTable, made from BASE_RECIPE_BOOK.

factoriax.engine.recipes.BASE_RECIPES: tuple[Recipe, ...] = (Recipe(output=7, inputs=((2, 1), (1, 1)), ticks=2, output_count=1, name='Iron Plate'), Recipe(output=8, inputs=((3, 1), (1, 1)), ticks=2, output_count=1, name='Copper Plate'), Recipe(output=9, inputs=((4, 1), (1, 1)), ticks=2, output_count=1, name='Tin Plate'), Recipe(output=10, inputs=((5, 1), (1, 1)), ticks=2, output_count=1, name='Wafer'), Recipe(output=11, inputs=((7, 1), (9, 1)), ticks=4, output_count=1, name='Frame'), Recipe(output=12, inputs=((8, 1), (10, 1)), ticks=4, output_count=1, name='Circuit'), Recipe(output=13, inputs=((8, 1), (9, 1)), ticks=4, output_count=1, name='Wire'), Recipe(output=14, inputs=((11, 1), (13, 1)), ticks=6, output_count=1, name='Motor'), Recipe(output=15, inputs=((12, 1), (13, 1)), ticks=6, output_count=1, name='Sensor'), Recipe(output=26, inputs=((7, 1), (8, 1)), ticks=4, output_count=1, name='Conveyor Belt'), Recipe(output=24, inputs=((7, 1), (13, 1)), ticks=6, output_count=1, name='Miner'), Recipe(output=27, inputs=((11, 1), (12, 1)), ticks=8, output_count=1, name='Assembler'), Recipe(output=25, inputs=((9, 1), (13, 1)), ticks=4, output_count=1, name='Pallet'), Recipe(output=28, inputs=((8, 1), (13, 1)), ticks=4, output_count=1, name='Arm'), Recipe(output=30, inputs=((7, 1), (18, 1)), ticks=6, output_count=1, name='Furnace'), Recipe(output=16, inputs=((14, 1), (9, 1)), ticks=8, output_count=1, name='Tier 1 Science Pack'), Recipe(output=17, inputs=((15, 1), (10, 1)), ticks=8, output_count=1, name='Tier 2 Science Pack'), Recipe(output=23, inputs=((17, 1), (15, 1)), ticks=8, output_count=1, name='Tier 3 Science Pack'), Recipe(output=29, inputs=((19, 6), (22, 4)), ticks=300, output_count=1, name='Rocket'), Recipe(output=31, inputs=((12, 2), (14, 2)), ticks=8, output_count=1, name='Science Lab'), Recipe(output=32, inputs=((9, 1), (1, 1)), ticks=4, output_count=1, name='Splitter'), Recipe(output=33, inputs=((8, 1), (1, 1)), ticks=4, output_count=1, name='Crossing'), Recipe(output=18, inputs=((6, 1), (1, 1)), ticks=4, output_count=1, name='Refractory'), Recipe(output=19, inputs=((11, 2), (7, 2)), ticks=8, output_count=1, name='Hull'), Recipe(output=20, inputs=((14, 2), (13, 1)), ticks=8, output_count=1, name='Engine Unit'), Recipe(output=21, inputs=((12, 2), (15, 2)), ticks=10, output_count=1, name='Avionics'), Recipe(output=22, inputs=((20, 1), (21, 1)), ticks=10, output_count=1, name='Rocket Core'))#

The recipes that come with the engine, in the order that gives them their ids.

The order is internal. execute_action maps a CRAFT_ action to an item and finds that item in output_to_recipe. The position of a recipe therefore never reaches the action space, and a new order for this tuple does not change the meaning of a craft action. Every recipe here has a matching CRAFT_ action. The book of a scenario does not have to do the same.

factoriax.engine.recipes.BASE_RECIPE_BOOK: RecipeBook = RecipeBook(recipes=(Recipe(output=7, inputs=((2, 1), (1, 1)), ticks=2, output_count=1, name='Iron Plate'), Recipe(output=8, inputs=((3, 1), (1, 1)), ticks=2, output_count=1, name='Copper Plate'), Recipe(output=9, inputs=((4, 1), (1, 1)), ticks=2, output_count=1, name='Tin Plate'), Recipe(output=10, inputs=((5, 1), (1, 1)), ticks=2, output_count=1, name='Wafer'), Recipe(output=11, inputs=((7, 1), (9, 1)), ticks=4, output_count=1, name='Frame'), Recipe(output=12, inputs=((8, 1), (10, 1)), ticks=4, output_count=1, name='Circuit'), Recipe(output=13, inputs=((8, 1), (9, 1)), ticks=4, output_count=1, name='Wire'), Recipe(output=14, inputs=((11, 1), (13, 1)), ticks=6, output_count=1, name='Motor'), Recipe(output=15, inputs=((12, 1), (13, 1)), ticks=6, output_count=1, name='Sensor'), Recipe(output=26, inputs=((7, 1), (8, 1)), ticks=4, output_count=1, name='Conveyor Belt'), Recipe(output=24, inputs=((7, 1), (13, 1)), ticks=6, output_count=1, name='Miner'), Recipe(output=27, inputs=((11, 1), (12, 1)), ticks=8, output_count=1, name='Assembler'), Recipe(output=25, inputs=((9, 1), (13, 1)), ticks=4, output_count=1, name='Pallet'), Recipe(output=28, inputs=((8, 1), (13, 1)), ticks=4, output_count=1, name='Arm'), Recipe(output=30, inputs=((7, 1), (18, 1)), ticks=6, output_count=1, name='Furnace'), Recipe(output=16, inputs=((14, 1), (9, 1)), ticks=8, output_count=1, name='Tier 1 Science Pack'), Recipe(output=17, inputs=((15, 1), (10, 1)), ticks=8, output_count=1, name='Tier 2 Science Pack'), Recipe(output=23, inputs=((17, 1), (15, 1)), ticks=8, output_count=1, name='Tier 3 Science Pack'), Recipe(output=29, inputs=((19, 6), (22, 4)), ticks=300, output_count=1, name='Rocket'), Recipe(output=31, inputs=((12, 2), (14, 2)), ticks=8, output_count=1, name='Science Lab'), Recipe(output=32, inputs=((9, 1), (1, 1)), ticks=4, output_count=1, name='Splitter'), Recipe(output=33, inputs=((8, 1), (1, 1)), ticks=4, output_count=1, name='Crossing'), Recipe(output=18, inputs=((6, 1), (1, 1)), ticks=4, output_count=1, name='Refractory'), Recipe(output=19, inputs=((11, 2), (7, 2)), ticks=8, output_count=1, name='Hull'), Recipe(output=20, inputs=((14, 2), (13, 1)), ticks=8, output_count=1, name='Engine Unit'), Recipe(output=21, inputs=((12, 2), (15, 2)), ticks=10, output_count=1, name='Avionics'), Recipe(output=22, inputs=((20, 1), (21, 1)), ticks=10, output_count=1, name='Rocket Core')))#

The RecipeBook that comes with the engine.

factoriax.engine.recipes.DEFAULT_RECIPE_TABLE: RecipeTable = RecipeTable(outputs=Array([ 7,  8,  9, 10, 11, 12, 13, 14, 15, 26, 24, 27, 25, 28, 30, 16, 17,        23, 29, 31, 32, 33, 18, 19, 20, 21, 22], dtype=int32), output_counts=Array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        1, 1, 1, 1, 1], dtype=int32), input_items=Array([[ 2,  1],        [ 3,  1],        [ 4,  1],        [ 5,  1],        [ 7,  9],        [ 8, 10],        [ 8,  9],        [11, 13],        [12, 13],        [ 7,  8],        [ 7, 13],        [11, 12],        [ 9, 13],        [ 8, 13],        [ 7, 18],        [14,  9],        [15, 10],        [17, 15],        [19, 22],        [12, 14],        [ 9,  1],        [ 8,  1],        [ 6,  1],        [11,  7],        [14, 13],        [12, 15],        [20, 21]], dtype=int32), input_counts=Array([[1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [1, 1],        [6, 4],        [2, 2],        [1, 1],        [1, 1],        [1, 1],        [2, 2],        [2, 1],        [2, 2],        [1, 1]], dtype=int32), ticks=Array([  2,   2,   2,   2,   4,   4,   4,   6,   6,   4,   6,   8,   4,          4,   6,   8,   8,   8, 300,   8,   4,   4,   4,   8,   8,  10,         10], dtype=int32), machine_type=Array([7, 7, 7, 7, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,        7, 4, 4, 4, 4], dtype=int32), output_to_recipe=Array([-1, -1, -1, -1, -1, -1, -1,  0,  1,  2,  3,  4,  5,  6,  7,  8, 15,        16, 22, 23, 24, 25, 26, 17, 10, 12,  9, 11, 13, 18, 14, 19, 20, 21],      dtype=int32), names=('Iron Plate', 'Copper Plate', 'Tin Plate', 'Wafer', 'Frame', 'Circuit', 'Wire', 'Motor', 'Sensor', 'Conveyor Belt', 'Miner', 'Assembler', 'Pallet', 'Arm', 'Furnace', 'Tier 1 Science Pack', 'Tier 2 Science Pack', 'Tier 3 Science Pack', 'Rocket', 'Science Lab', 'Splitter', 'Crossing', 'Refractory', 'Hull', 'Engine Unit', 'Avionics', 'Rocket Core'))#

Default RecipeTable, made from BASE_RECIPE_BOOK.

factoriax.engine.recipes.MAX_RECIPE_INPUTS: int = 2#

Largest number of different input item types in one recipe. This is not a limit on amounts. A recipe can ask for any count of each type, so Hull consumes four items, 2 frames and 2 iron plates, and still has two inputs.

This is a fixed engine limit, not a property of BASE_RECIPES, and it applies to a hand craft as much as to a machine. The value is 2 because EnvState.ent_asm_in_type gives a machine two input slots. The inventory of a player can feed a wider recipe, but the engine holds both paths to one width, so both read the same rows. A larger value needs wider state arrays and a more general recipe matcher, which today tests two slots in both orders by hand. RecipeBook refuses a wider recipe.

factoriax.engine.recipes.NUM_RECIPES: int = 27#

Number of recipes in BASE_RECIPES. A scenario supplies its own book, so engine code takes the size of its loops from table.outputs.shape[0].

factoriax.engine.recipes.RECIPE_NAMES: list[str] = ['Iron Plate', 'Copper Plate', 'Tin Plate', 'Wafer', 'Frame', 'Circuit', 'Wire', 'Motor', 'Sensor', 'Conveyor Belt', 'Miner', 'Assembler', 'Pallet', 'Arm', 'Furnace', 'Tier 1 Science Pack', 'Tier 2 Science Pack', 'Tier 3 Science Pack', 'Rocket', 'Science Lab', 'Splitter', 'Crossing', 'Refractory', 'Hull', 'Engine Unit', 'Avionics', 'Rocket Core']#

Names of BASE_RECIPES. The table of a scenario carries its own names in RecipeTable.names.

class factoriax.engine.recipes.Recipe(output: int, inputs: tuple[tuple[int, int], ...], ticks: int, output_count: int = 1, name: str = '')#

One recipe: the item that it produces, and the cost to produce it.

The identity of a recipe is its output item, its input item types, and the machine type that those imply. BASE_RECIPES fixes that identity, and a change to it is a code change. The numbers are balance, and RecipeBalance overrides them and leaves the identity alone.

output#

ItemType value that this recipe produces. The value is unique inside a RecipeBook, which lets RecipeTable.output_to_recipe map an item back to one recipe.

Type:

int

inputs#

(item_type, count) pairs that one craft consumes, one or two of them. The order joins each count to its item, and it fixes the order in which a RecipeOverride must list input_counts. The order does not decide which input slot holds an item, because the matcher accepts both slot orders.

Type:

tuple[tuple[int, int], …]

ticks#

Steps that an assembler or a furnace takes to finish one craft. A hand craft ignores this field and finishes in the same step.

Type:

int

output_count#

Items that one craft produces.

Type:

int

name#

Label for the UI. This is not an identifier. The engine addresses a recipe by its position in the book.

Type:

str

property machine_type: int#

Return the machine kind that runs this recipe.

The value comes from the output item, and nothing stores it. A recipe therefore cannot declare a machine that disagrees with its output.

Returns:

Machine.FURNACE for the smelted outputs, Machine.ASSEMBLER for every other output.

Return type:

int

class factoriax.engine.recipes.RecipeBalance(overrides: tuple[tuple[int, RecipeOverride], ...] = ())#

Balance overrides for a whole game, with the output item as the key.

A scenario tunes this object and does not copy the recipe table. Build one, apply it with RecipeBook.with_balance(), then turn the result into a RecipeTable.

The overrides are a tuple of pairs and not a dict, so the class stays hashable and safe to share across JIT cache keys. A lookup is linear, and that costs nothing here. The list is small, and code reads it when it builds a book, never in the hot path.

overrides#

(output_item, override) pairs. An output can appear one time at most. The class ignores an override for an item that no recipe in the book produces.

Type:

tuple[tuple[int, factoriax.engine.recipes.RecipeOverride], …]

Examples

>>> from factoriax.engine.constants import ItemType
>>> from factoriax.engine.recipes import (
...     BASE_RECIPE_BOOK,
...     RecipeBalance,
...     RecipeOverride,
...     RecipeTable,
... )
>>> balance = RecipeBalance(
...     overrides=((int(ItemType.IRON_PLATE), RecipeOverride(ticks=1)),)
... )
>>> table = RecipeTable.from_book(BASE_RECIPE_BOOK.with_balance(balance))
>>> int(table.ticks[0])
1
get(output_item: int) → RecipeOverride | None#

Find the override that applies to one recipe.

Parameters:

output_item – ItemType value that the recipe produces.

Returns:

The matching override, or None when this balance leaves that recipe on its base numbers.

Return type:

RecipeOverride or None

class factoriax.engine.recipes.RecipeBook(recipes: tuple[Recipe, ...])#

A tuple of recipes, validated against the rules that the engine needs.

Construction tests four rules. A bad book therefore fails at once and names the recipe at fault, and never builds arrays that fail later at runtime:

  1. Input arity. Every recipe takes 1 to MAX_RECIPE_INPUTS input types. A machine holds that number of input slots in EnvState, so the engine cannot feed a wider recipe.

  2. No repeated input item. A recipe names each input item one time at most. factoriax.engine.crafting.can_afford_recipe() tests each slot on its own. With a repeated item, one slot alone can pass the test, and the craft then takes the count of the player below zero.

  3. Unique outputs. One recipe at most produces each item. RecipeTable.output_to_recipe maps an item back to one recipe. Two recipes for the same item also break the yield arithmetic in factoriax.engine.crafting.craft_recipe().

  4. Unique input set for each machine and arity. No two recipes on the same machine kind, with the same number of inputs, consume the same unordered set of input item types. The recipe matcher selects a recipe by input item types alone. The counts of a recipe only gate whether the match fires, so counts cannot separate two recipes, and such a pair is ambiguous. Two recipes can share an input set at different arities, because the matcher gates a 1-input recipe on an empty second slot. Two recipes can also share one across machine kinds, because the matcher divides by machine first.

recipes#

The validated records, in order. The position of a recipe is its id, and that id indexes every array in RecipeTable.

Type:

tuple[factoriax.engine.recipes.Recipe, …]

Raises:

ValueError – If a recipe breaks one of the four rules above.

with_balance(balance: RecipeBalance) → RecipeBook#

Apply balance overrides, and return a new validated book.

The recipe order, the output items, the machine types, and the input item types all pass through unchanged. Only the counts and ticks change. A recipe whose output the balance does not name passes through as it is. The new book runs the same construction tests, and those tests still hold, because an override touches no part of the identity of a recipe.

Parameters:

balance – Overrides, with the output item as the key. The method ignores an override for an item that this book does not produce, and reports nothing.

Returns:

A new book with the overrides applied. The method returns the same book unchanged when balance holds no overrides.

Return type:

RecipeBook

Raises:

ValueError – If the length of input_counts in an override does not match the number of inputs of the target recipe, or if an input count, an output_count, or a ticks value comes out negative.

class factoriax.engine.recipes.RecipeOverride(input_counts: tuple[int, ...] | None = None, output_count: int | None = None, ticks: int | None = None)#

Balance numbers that replace the numbers of one recipe.

A field that holds None keeps the value of the base recipe. This class cannot tune the identity of a recipe: the output item, the input item types, and the machine. To change the flow of items, edit BASE_RECIPES.

input_counts#

New count for each input, in the input order of the base recipe. The length must match the number of inputs of that recipe.

Type:

tuple[int, …] | None

output_count#

New number of items that one craft produces.

Type:

int | None

ticks#

New craft time on a machine, in steps.

Type:

int | None

class factoriax.engine.recipes.RecipeTable(outputs: Array, output_counts: Array, input_items: Array, input_counts: Array, ticks: Array, machine_type: Array, output_to_recipe: Array, names: tuple[str, ...] = ())#

Stacked JAX arrays that come from a RecipeBook.

This class holds every per-recipe number that the engine reads inside JIT-compiled code: the machine cycle match, the craft yield, and the action dispatch. It travels on EnvParams, so the numbers reach traced code as PyTree leaves, and not as Python globals inside the XLA graph. Balance is therefore tunable at runtime with no new compile. RecipeBook fixes the recipe count and the arity at construction, so every balance overlay gives the same shapes.

A recipe id indexes the rows, and that id is the position of the recipe in the book. n below is the number of recipes.

outputs#

Item that each recipe produces. Shape (n,), int32.

Type:

jax.jaxlib._jax.Array

output_counts#

Items that one craft produces. Shape (n,), int32.

Type:

jax.jaxlib._jax.Array

input_items#

Input item in each slot. Shape (n, MAX_RECIPE_INPUTS), int32. A recipe with one input pads the unused slot with ItemType.EMPTY.

Type:

jax.jaxlib._jax.Array

input_counts#

Items that each slot consumes, in the same order as input_items. Shape (n, MAX_RECIPE_INPUTS), int32. A padded slot holds 0.

Type:

jax.jaxlib._jax.Array

ticks#

Steps that an assembler or a furnace needs for one craft. Shape (n,), int32.

Type:

jax.jaxlib._jax.Array

machine_type#

Machine value that runs each recipe. Shape (n,), int32.

Type:

jax.jaxlib._jax.Array

output_to_recipe#

Recipe id that produces each item, or -1 for an item that no recipe produces. Shape (len(ItemType),), int32. An item indexes this array, not a recipe, so it is the one array here with a length that is not n.

Type:

jax.jaxlib._jax.Array

names#

Label for each recipe id, in the same order as outputs. This field is static and not a traced leaf, so the UI can label the recipes of a scenario.

Type:

tuple[str, …]

classmethod from_book(book: RecipeBook) → RecipeTable#

Turn a RecipeBook into stacked JAX arrays.

The book validated itself at construction, so this method changes only shapes and dtypes and runs no further tests. The rows keep the recipe order of the book. A 1-input recipe gets the padding (EMPTY, 0), so every row has MAX_RECIPE_INPUTS input slots.

Parameters:

book – A validated book. Its recipe count and its arity fix the shapes of every array in the result.

Returns:

The stacked arrays, and the recipe names as static data.

Return type:

RecipeTable

replace(**updates)#

Returns a new object replacing the specified fields with new values.