factoriax.engine.recipes#
Recipe definitions for the Factoriax environment.
Recipe data sits in three layers:
Recipeis one record. It holds the item that the recipe produces, the items that it consumes, and the time that it takes.RecipeBookis a validated tuple of records. It enforces the rules that the recipe matcher of the engine depends on.RecipeTableturns 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
Largest number of different input item types in one recipe. |
|
The recipes that come with the engine, in the order that gives them their ids. |
|
Number of recipes in |
|
Names of |
|
The |
|
Default |
- 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_actionmaps aCRAFT_action to an item and finds that item inoutput_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 matchingCRAFT_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
RecipeBookthat 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 fromBASE_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 becauseEnvState.ent_asm_in_typegives 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.RecipeBookrefuses 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 fromtable.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 inRecipeTable.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_RECIPESfixes that identity, and a change to it is a code change. The numbers are balance, andRecipeBalanceoverrides them and leaves the identity alone.- output#
ItemTypevalue that this recipe produces. The value is unique inside aRecipeBook, which letsRecipeTable.output_to_recipemap an item back to one recipe.- Type:
- 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 aRecipeOverridemust listinput_counts. The order does not decide which input slot holds an item, because the matcher accepts both slot orders.
- 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:
- name#
Label for the UI. This is not an identifier. The engine addresses a recipe by its position in the book.
- Type:
- 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.FURNACEfor the smelted outputs,Machine.ASSEMBLERfor every other output.- Return type:
- 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 aRecipeTable.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 –
ItemTypevalue that the recipe produces.- Returns:
The matching override, or
Nonewhen 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:
Input arity. Every recipe takes 1 to
MAX_RECIPE_INPUTSinput types. A machine holds that number of input slots inEnvState, so the engine cannot feed a wider recipe.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.Unique outputs. One recipe at most produces each item.
RecipeTable.output_to_recipemaps an item back to one recipe. Two recipes for the same item also break the yield arithmetic infactoriax.engine.crafting.craft_recipe().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:
- 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
tickschange. 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
balanceholds no overrides.- Return type:
- Raises:
ValueError – If the length of
input_countsin an override does not match the number of inputs of the target recipe, or if an input count, anoutput_count, or aticksvalue 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
Nonekeeps 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, editBASE_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.
- 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.RecipeBookfixes 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.
nbelow 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 withItemType.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#
Machinevalue that runs each recipe. Shape(n,), int32.- Type:
jax.jaxlib._jax.Array
- output_to_recipe#
Recipe id that produces each item, or
-1for 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 notn.- 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.
- classmethod from_book(book: RecipeBook) RecipeTable#
Turn a
RecipeBookinto 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 hasMAX_RECIPE_INPUTSinput 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:
- replace(**updates)#
Returns a new object replacing the specified fields with new values.