factoriax.engine.tables#

Derived JAX arrays and lookup tables for the Factoriax engine.

factoriax.engine.constants holds the pure-Python definitions. This module turns them into the jnp arrays that engine code reads. It builds every array once, at import.

The arrays have two shapes. A gather table covers the full range of the value that indexes it: an ItemType, a Machine, a BlockType, or a Direction. It holds a neutral value at every position that has no entry. A traced index therefore never falls outside it, and a lookup needs no membership test first. A membership set, such as MINEABLE_BLOCKS or SOLID_BLOCKS, holds only the values that belong to the set. Code compares a value against such an array and never indexes it.

Both shapes keep the lookups of traced code branchless under jax.jit.

Module Attributes

DIRECTIONS

(dx, dy) step per Direction value.

SPLITTER_PERP_OUTPUTS

Output sides of a splitter, indexed by ent_direction.

CROSSING_AXIS_DIRS

Output direction of each crossing axis, indexed by the packed ent_direction.

CROSSING_DIAGONAL

Diagonal glyph for each crossing encoding, for the display only.

CROSSING_VERT_SLOT

Slot index of the vertical flow buffer in EnvState.ent_asm_in_type and EnvState.ent_asm_in_count.

CROSSING_HORIZ_SLOT

Slot index of the horizontal flow buffer, in the same two arrays as CROSSING_VERT_SLOT.

MINEABLE_BLOCKS

The block types that a player can mine, as a membership set and not as a table to index.

SOLID_BLOCKS

The block types that hold no player and no machine, also a membership set.

BLOCK_TO_ITEM_ARRAY

Item that each block gives to the player who mines it, indexed by BlockType.

ITEM_TO_MACHINE_ARRAY

Machine that an item becomes on the map, indexed by ItemType.

MACHINE_TO_ITEM_ARRAY

Item that a machine gives back after a pickup, indexed by Machine value.

PLACEABLE_ITEMS

The placeable item ids, as a membership set.

SCIENCE_PACK_INDEX

Position of an item in SCIENCE_PACK_TYPES, indexed by ItemType.

PLAYER_MAX_STACK

Number of items of one type that a player can carry, indexed by ItemType.

MACHINE_MAX_STACK

Items that one buffer slot holds, indexed by Machine value.

MACHINE_HAS_INPUT_SLOTS

Whether a machine takes deliveries into ent_asm_in, indexed by Machine value.

MACHINE_HEALTH

Hit points of a machine at the moment of placement.

MACHINE_MAX_HEALTH

Hit points of a placed machine at the start, indexed by Machine value.

factoriax.engine.tables.BLOCK_TO_ITEM_ARRAY = Array([0, 0, 0, 0, 2, 3, 1, 4, 5, 6], dtype=int32)#

Item that each block gives to the player who mines it, indexed by BlockType. Shape (len(BlockType),), int32. A block that a player cannot mine holds ItemType.EMPTY. A mine action on it therefore gives item 0, and the caller needs no separate test.

factoriax.engine.tables.CROSSING_AXIS_DIRS = Array([[0, 0],        [4, 2],        [4, 1],        [3, 2],        [3, 1]], dtype=int8)#

Output direction of each crossing axis, indexed by the packed ent_direction. Row e is (vertical_output, horizontal_output) for encoding e in 1..4. Shape (5, 2), dtype int8. A crossing runs a vertical flow and a horizontal flow at the same time, and packs both into the one direction byte. An axis takes items from the neighbour opposite to its output direction. If vertical_output is DOWN, the vertical flow reads the tile above and writes the tile below. The two input sides therefore always meet at a corner. Row 0 is (0, 0). A crossing with no direction moves nothing on either axis.

factoriax.engine.tables.CROSSING_DIAGONAL: tuple[str, ...] = ('', '\\', '/', '/', '\\')#

Diagonal glyph for each crossing encoding, for the display only. A backslash marks a NW-SE diagonal. A forward slash marks a NE-SW diagonal. The diagonal runs from the corner where the two input sides of the encoding meet, to the opposite corner. Index 0 is the empty string, because a crossing with no direction has no input sides. The simulation never reads this value. Only the crossing icon reads it.

factoriax.engine.tables.CROSSING_HORIZ_SLOT: int = 1#

Slot index of the horizontal flow buffer, in the same two arrays as CROSSING_VERT_SLOT.

factoriax.engine.tables.CROSSING_VERT_SLOT: int = 0#

Slot index of the vertical flow buffer in EnvState.ent_asm_in_type and EnvState.ent_asm_in_count. A crossing uses the two assembler input slots as one buffer for each axis. Each axis reads and writes its own slot only, so the two flows cannot mix.

factoriax.engine.tables.DIRECTIONS = Array([[ 0,  0],        [-1,  0],        [ 1,  0],        [ 0, -1],        [ 0,  1]], dtype=int32)#

(dx, dy) step per Direction value. Shape (5, 2), int32. Row 0 is (0, 0) for the value Direction leaves unnumbered, so a machine with no direction moves nothing. dy increases downward, which matches the row order of EnvState.map. UP is therefore (0, -1).

factoriax.engine.tables.ITEM_TO_MACHINE_ARRAY = Array([ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,         0,  0,  0,  0,  0,  0,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10],      dtype=int32)#

Machine that an item becomes on the map, indexed by ItemType. Shape (NUM_ITEM_TYPES,), int32. An item that no action can place holds Machine.NONE, and that is how placement refuses it.

factoriax.engine.tables.MACHINE_HAS_INPUT_SLOTS = Array([False, False, False, False,  True, False, False,  True,  True,        False, False], dtype=bool)#

Whether a machine takes deliveries into ent_asm_in, indexed by Machine value. Shape (len(Machine),), bool. The value is True for assemblers, furnaces, and science labs, and that is the one property those three share. An assembler and a furnace consume the slots to run a recipe. A science lab consumes them directly, in factoriax.engine.step.run_labs().

Every path into those slots reads this array, and no path lists the three machine kinds again. A new kind with input slots therefore needs one change only. This array does not tell a caller whether a machine runs recipes, because a science lab does not run recipes. factoriax.engine.machines tests ASSEMBLER and FURNACE directly for that question.

A CROSSING is False here, although it keeps its two axis buffers in the same two columns. Nothing delivers into a crossing by slot. The belt pass selects the axis from the direction of travel.

factoriax.engine.tables.MACHINE_HEALTH: int = 256#

Hit points of a machine at the moment of placement. The value is the same for every machine kind. MACHINE_MAX_HEALTH repeats it over the Machine range, so a caller can index it with a machine type and needs no special case.

factoriax.engine.tables.MACHINE_MAX_HEALTH = Array([256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256], dtype=int16)#

Hit points of a placed machine at the start, indexed by Machine value. Shape (len(Machine),), int16 to match EnvState.ent_health. A repair clamps to this value, and factoriax.engine.placement allows a pickup only at full health.

factoriax.engine.tables.MACHINE_MAX_STACK = Array([   0,   64,  256,    3, 1000,    1,    0, 1000, 1000,    2,    2],      dtype=int16)#

Items that one buffer slot holds, indexed by Machine value. Shape (len(Machine),), int16 to match EnvState.ent_buf_count. The unit is items, not stacks. Zero means that the machine holds nothing. ARM holds one, because it carries a single item in transit. A transfer in factoriax.engine.machines compares the count at the destination against this value. It refuses a move into a full buffer and leaves the source unchanged.

factoriax.engine.tables.MACHINE_TO_ITEM_ARRAY = Array([ 0, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33], dtype=int32)#

Item that a machine gives back after a pickup, indexed by Machine value. Shape (len(Machine),), int32. This is the inverse of ITEM_TO_MACHINE_ARRAY, with ItemType.EMPTY at Machine.NONE.

factoriax.engine.tables.MINEABLE_BLOCKS = Array([6, 4, 5, 7, 8, 9], dtype=int32)#

The block types that a player can mine, as a membership set and not as a table to index. These are the keys of BLOCK_TO_ITEM. Any other block gives nothing.

factoriax.engine.tables.PLACEABLE_ITEMS = Array([24, 25, 26, 27, 28, 29, 30, 31, 32, 33], dtype=int32)#

The placeable item ids, as a membership set. The contents and the order are the same as PLACEABLE_ITEM_LIST. That order is the palette order of the play UI and carries no meaning here.

factoriax.engine.tables.PLAYER_MAX_STACK = Array([   0, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024,        1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024,  128,  128,  128,         128, 1024,  128,  128,  128,  128,  128,  128,  128,  128,  128,         128], dtype=int32)#

Number of items of one type that a player can carry, indexed by ItemType. Shape (NUM_ITEM_TYPES,), int32. The unit is items, not stacks. EMPTY holds 0, so no count can build up in the slot that marks an empty slot. The engine refuses a craft or a pickup that passes this limit, and does not clip it. The inputs of the craft stay in the inventory, and the machine stays on the map.

factoriax.engine.tables.SCIENCE_PACK_INDEX = Array([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  0,         1, -1, -1, -1, -1, -1,  2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],      dtype=int8)#

Position of an item in SCIENCE_PACK_TYPES, indexed by ItemType. Shape (NUM_ITEM_TYPES,), int8. An item that is not a science pack holds -1. The step function uses the position as a segment id when it totals EnvState.science_consumed_step.

factoriax.engine.tables.SOLID_BLOCKS = Array([3, 1], dtype=int32)#

The block types that hold no player and no machine, also a membership set. Both factoriax.engine.step.is_position_walkable() and factoriax.engine.placement.is_valid_placement_tile() read it, so the two agree on what is solid.

factoriax.engine.tables.SPLITTER_PERP_OUTPUTS = Array([[0, 0],        [3, 4],        [3, 4],        [1, 2],        [1, 2]], dtype=int8)#

Output sides of a splitter, indexed by ent_direction. Row d holds the two Direction values that are perpendicular to facing d, and a splitter with facing d pushes to those two sides. Shape (5, 2), dtype int8. A splitter stores the direction in which items move through it, the same convention that a conveyor belt uses. A splitter with facing d therefore takes items from the neighbour on its -d side. The two entries are in ascending Direction order, and that order carries no meaning, because a splitter treats both sides in the same way. Row 0 is (0, 0), a pair that matches no direction, so an entity with no direction never pushes to a real side.