factoriax.engine.step#

Advance the environment one step.

A step is one player action, and then one run of the whole factory. factoriax_step() holds the order. The action lands first, then every machine on the map moves, then the science labs consume their packs, then the clock advances. An agent therefore sees the result of its action only in the next observation.

The actions are compound. There is no cursor that selects a slot and no separate key that acts on it. Each action names its own target: one action for each placeable item, one for each craftable item, and one for each depositable item. The action space is therefore wide, and each action means something on its own. No field in the state has to hold a selection.

Every player action here targets one tile, the tile that the player faces. Each one uses a mask and not a branch. An action that cannot happen writes the state back unchanged and reports nothing.

A position is (x, y), the column first. Every grid lookup is [y, x].

The player transfers here obey the same per-machine capacities as the machine passes in factoriax.engine.machines. Both read MACHINE_MAX_STACK. A player cannot put more into a belt than a miner can.

factoriax.engine.step.deposit_to_adjacent(state: EnvState, player_idx: int | Array, item_type: int | Array) → EnvState#

Hand one item to the machine in front of the player.

One action moves one item, for every machine kind. withdraw_from_adjacent() is different: it empties a whole slot in one action.

The machine decides where the item lands. An assembler, a furnace, or a science lab takes it into ent_asm_in. It uses slot 0 when that slot is empty or already holds the same item, and slot 1 in every other case. Every other machine takes it into ent_buf, and only when the buffer is empty or already holds the same item.

A miner is the one machine that always refuses a deposit. Its buffer is an output.

Both routes obey MACHINE_MAX_STACK, the same table that the machine passes read. A belt therefore fills to 3, and a rocket refuses every item. A refused deposit leaves the item in the inventory of the player.

Parameters:
  • state – State to read.

  • player_idx – Player that deposits the item.

  • item_type – Item to hand over. The action itself names it, and there is no selection cursor.

Returns:

New state, with player_inventory, ent_buf_type, ent_buf_count, ent_asm_in_type, and ent_asm_in_count updated. The state is unchanged when the function refuses the deposit.

Return type:

EnvState

factoriax.engine.step.factoriax_step(rng: Array, state: EnvState, action: int | Array, params: EnvParams) → EnvState#

Apply one action, run the factory, and advance the clock.

The order is fixed and is part of the contract. The player acts first. Then factoriax.engine.machines.update_all_machines() runs every machine. Then run_labs() consumes the packs in the science labs. Then timestep increases by 1.

The player acts first, so a player deposit reaches a machine in time for that machine to use it in the same step. The labs run last, so a pack that an arm delivered in this step is also consumed in this step.

Only state.selected_player acts. Every other player keeps its position. There is no per-player action vector here.

Parameters:
  • rng – The function does not read this argument. Nothing in a step is random. It is present so that the signature matches the gymnax-style step API around it.

  • state – State to advance.

  • action – Action for the selected player.

  • params – The function passes this to the action handlers and to the machine passes.

Returns:

New state, one step later, with timestep one higher. science_consumed_step holds the totals of this step, not the totals of the step before.

Return type:

EnvState

factoriax.engine.step.get_block_at(state: EnvState, position: Array) → Array#

Read the terrain block at a position.

A position outside the map returns BlockType.OUT_OF_BOUNDS, and the function raises nothing. A caller can therefore read past an edge with no test first. The function clips the index before the read, so nothing indexes outside the array.

Parameters:
  • state – State to read.

  • position – Tile to read as (x, y), column first.

Returns:

Scalar block id, or BlockType.OUT_OF_BOUNDS outside the map.

Return type:

jax.Array

factoriax.engine.step.is_game_over(state: EnvState, params: EnvParams) → Array#

Report whether the episode has run out of time.

Time is the only end condition in the base engine. There is no state in which a player loses, and no goal that stops a run early. A scenario that wants one adds it in a wrapper.

Parameters:
  • state – State to test.

  • params – Supplies max_timesteps.

Returns:

Scalar bool. True once timestep reaches max_timesteps.

Return type:

jax.Array

factoriax.engine.step.is_position_in_bounds(position: Array, map_width: int, map_height: int) → Array#

Report whether a position falls on the map.

Parameters:
  • position – Tile to test as (x, y), column first.

  • map_width – Map width in tiles.

  • map_height – Map height in tiles.

Returns:

Scalar bool. The value is False for a negative coordinate, and for a coordinate at the far edge or past it.

Return type:

jax.Array

factoriax.engine.step.is_position_walkable(state: EnvState, position: Array) → Array#

Report whether a player can stand on a position.

Two things block a tile: a block in SOLID_BLOCKS, such as stone or water, and a machine. A conveyor belt is the one exception, and a player can walk over it. A belt line therefore does not close a player out of its own factory.

The function ignores the other players, so two players can stand on one tile.

A position outside the map fails, because OUT_OF_BOUNDS is a solid block.

Parameters:
  • state – State to read.

  • position – Tile to test as (x, y), column first.

Returns:

Scalar bool.

Return type:

jax.Array

factoriax.engine.step.mine_block(state: EnvState, player_idx: int | Array, params: EnvParams) → EnvState#

Mine the block on the tile in front of the player.

The function targets the tile in the facing direction of the player. It does nothing in four cases: the tile is outside the map, no player can mine that block, the tile holds no ore left, or the inventory of the player has no room for the yield. An action that works extracts up to params.player_mining_yield units. The ore left in the tile and the stack space in the inventory both limit that amount.

This function is the only way to get ore with no miner on the map, and it is how a run starts. A player must craft the first miner from ore that they mined by hand.

A tile with no ore left becomes BlockType.DIRT, the same as a tile under a miner.

Parameters:
  • state – State to read.

  • player_idx – Player that mines the block.

  • params – Supplies player_mining_yield, the amount for one action before the tile limit and the inventory limit apply.

Returns:

New state, with map, player_inventory, block_resources, and items_mined updated. The state is unchanged when the action mined nothing. This function adds every mined item to items_mined, and not only the five ore types. factoriax.engine.machines.run_miners() counts the five ore types only.

Return type:

EnvState

factoriax.engine.step.move_player(state: EnvState, action: int | Array, player_idx: int | Array) → EnvState#

Move a player one tile, or turn them without moving.

The facing matters more than the position, because every other player action targets the tile in front. Two actions set it. A movement action turns the player, and then moves them when the target tile is walkable. A FACE_ action turns them and does not move them.

A movement action into a blocked tile still turns the player, and this is deliberate. One action can therefore always aim at a wall, a machine, or an ore patch, and the next action can act on it.

An action that is neither a movement nor a FACE_ leaves the position and the facing as they were. A caller can therefore call this function for every action.

Parameters:
  • state – State to read.

  • action – Action id. Only the four movement values and the four FACE_ values have an effect.

  • player_idx – Player to move.

Returns:

New state, with player_positions and player_directions updated for that player.

Return type:

EnvState

factoriax.engine.step.run_labs(state: EnvState) → EnvState#

Consume every science pack sitting in any SCIENCE_LAB input slot.

A lab is a sink. It has no countdown and no output. It consumes everything in its two input slots at this point in the step. The function records the amount for each type in science_consumed_step, and the reward functions and the achievements read that field. factoriax_step sets the field to zero at the start of every step, so it is a per-step delta and not a total.

The function consumes science packs only. An item that is not a pack stays in the input slot of the lab and blocks that slot.

The function reads ent_asm_in only. It never touches the ent_buf of a lab, and that is why a belt that points at a lab is a dead end. See factoriax.engine.machines.run_conveyor_belts().

Parameters:

state – State to read.

Returns:

New state, with the consumed slots of every lab cleared in ent_asm_in_type and ent_asm_in_count, and science_consumed_step set to the totals of this step for each type. The totals are zero when no lab held a pack.

Return type:

EnvState

factoriax.engine.step.withdraw_from_adjacent(state: EnvState, player_idx: int | Array) → EnvState#

Withdraw from the output slot of the machine in front of the player.

A machine exposes exactly one output slot at a time. That slot is ent_asm_out for an assembler or a furnace inside a cycle, and ent_buf in every other case. One WITHDRAW action takes as many items as fit: the smaller of the count in the slot and the free inventory space of the player. An agent therefore does not spend 100 steps to empty a pallet of 100 ore, one item at a time.

The contents decide which slot the function reads, not the machine kind. It reads ent_asm_out when that slot holds items, and ent_buf in every other case. In practice the two never hold items at the same time, because an assembler and a furnace write only asm_out, and a buffer machine writes only buf.

An empty slot loses its item type as well as its count, because the rest of the engine reads a cleared type as free to accept any item.

Parameters:
  • state – State to read.

  • player_idx – Player that withdraws the items.

Returns:

New state, with player_inventory, ent_buf_type, ent_buf_count, ent_asm_out_type, and ent_asm_out_count updated. The state is unchanged when the tile holds no machine, when the machine holds nothing, or when the player has no room for the item.

Return type:

EnvState