factoriax.engine.levels#

Describe a starting world and turn it into an EnvState.

A Level is the static half of a world: the block layout, and, where the author gives them, the ore amounts, the machines already on the map, and the spawn tiles of the players. It holds no live state and no timestep. It is plain numpy, so it writes to JSON and reads back unchanged.

The level does not fix the number of players. The build decides that. build_state() takes the count as an argument, so one level serves a single-agent run and a four-agent run with no edit.

There are two ways in. LevelBuilder writes a level tile by tile, and the editor and the hand-written scenarios use it. generate_state() uses no Level at all and makes a state from a random key. An environment calls it when a scenario supplies no level.

Typical usage:

from pathlib import Path

from factoriax.engine.constants import BlockType
from factoriax.engine.levels import (
    LevelBuilder, build_state, get_level, load_level, save_level,
)

# Load a built-in level and build a state for two players.
level = get_level("15x15_resources")
state = build_state(level, num_players=2)

# Define a custom level programmatically.
level = (
    LevelBuilder(8, 8)
    .fill_rect(0, 0, 3, 3, BlockType.COAL)
    .build("coal_corner")
)

# Round-trip through JSON.
save_level(level, Path("my_level.json"))
level = load_level(Path("my_level.json"))

Module Attributes

LEVELS

Every built-in level, with the name as the key.

factoriax.engine.levels.LEVELS: dict[str, Level] = {'15x15_resources': Level(name='15x15_resources', map_width=15, map_height=15, block_map=array([[6, 6, 6, 6, 2, 2, 2, 2, 2, 2, 2, 5, 5, 5, 5],        [6, 6, 6, 6, 2, 2, 2, 2, 2, 2, 2, 5, 5, 5, 5],        [6, 6, 6, 6, 2, 2, 2, 2, 2, 2, 2, 5, 5, 5, 5],        [6, 6, 6, 6, 2, 2, 2, 2, 2, 2, 2, 5, 5, 5, 5],        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],        [4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]], dtype=int32), block_resources=array([[30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0, 30000, 30000, 30000, 30000],        [30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0, 30000, 30000, 30000, 30000],        [30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0, 30000, 30000, 30000, 30000],        [30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0, 30000, 30000, 30000, 30000],        [    0,     0,     0,     0,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [    0,     0,     0,     0,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [    0,     0,     0,     0,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [    0,     0,     0,     0,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [    0,     0,     0,     0,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [    0,     0,     0,     0,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [    0,     0,     0,     0,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0],        [30000, 30000, 30000, 30000,     0,     0,     0,     0,     0,             0,     0,     0,     0,     0,     0]], dtype=int32), machine_types=None, machine_directions=None, machine_inventory=None, player_inventory=None, player_inventories=None, player_positions=None)}#

Every built-in level, with the name as the key.

class factoriax.engine.levels.Level(name: str, map_width: int, map_height: int, block_map: ndarray, block_resources: ndarray | None = None, machine_types: ndarray | None = None, machine_directions: ndarray | None = None, machine_inventory: ndarray | None = None, player_inventory: list[tuple[int, int]] | None = None, player_inventories: dict[int, list[tuple[int, int]]] | None = None, player_positions: list[tuple[int, int]] | None = None)#

A starting world, as plain arrays that survive a JSON round trip.

Every field after block_map is optional. A field that holds None means “let build_state() decide”, and not “empty”. With no block_resources, default_resources() fills the ore tiles. With no player_positions, the build spreads the players along the middle row. A field that holds an array of zeros is a different instruction. It means that the world starts with nothing.

An array is indexed [row, column], so block_map[y][x]. A position list holds (x, y) pairs. The two orders are not the same, and that matters when a person writes a level by hand.

name#

Identifier for the level. get_level() finds a level by it.

Type:

str

map_width, map_height

Tile dimensions. The class validates every array field against them.

block_map#

BlockType of each tile. Shape (map_height, map_width). This is the one required field.

Type:

numpy.ndarray

block_resources#

Ore units in each tile, or None to take them from block_map.

Type:

numpy.ndarray | None

machine_types#

Machine on each tile, NONE where the tile is clear, or None for a world with no machines.

Type:

numpy.ndarray | None

machine_directions#

Facing of each tile, 0 where nothing set it. The value has no meaning on a tile with no machine.

Type:

numpy.ndarray | None

machine_inventory#

Items in the machine on each tile, shape (map_height, map_width, NUM_ITEM_TYPES). An item indexes it and it records no slot, so build_state() must decide which slot holds each item.

Type:

numpy.ndarray | None

player_inventory#

(item, count) pairs for every player.

Type:

list[tuple[int, int]] | None

player_inventories#

Contents for one player, with the player index as the key. For a player that it names, it replaces player_inventory and does not add to it. An index past the player count has no effect.

Type:

dict[int, list[tuple[int, int]]] | None

player_positions#

(x, y) spawn tile of each player. The build sets each listed tile to DIRT, so a spawn never lands in ore or water. When the field is set, the list must hold exactly one entry for each player of the state.

Type:

list[tuple[int, int]] | None

Raises:

ValueError – If an array field disagrees with map_width or map_height. __post_init__ raises it, so a malformed level fails at construction and not at render time.

class factoriax.engine.levels.LevelBuilder(width: int, height: int, default_block: BlockType = BlockType.DIRT)#

Build a Level a tile at a time.

Every method that writes returns self, so the calls chain, and build() ends the chain. The builder starts from a canvas of one block type. It allocates an optional array only when a method first writes to it. A field that no method touched therefore stays None in the finished level, and build_state() applies its own default.

A builder is reusable. build() copies each array out, so a later edit cannot change a level that the builder already produced.

A coordinate is (x, y), with the origin at the top left. This is the opposite order from the [row, column] arrays below it.

Example:

level = (
    LevelBuilder(15, 15)
    .fill_rect(0, 0, 4, 4, BlockType.COAL)
    .fill_rect(11, 0, 4, 4, BlockType.COPPER)
    .build("my_level")
)
build(name: str) → Level#

End the chain and return the finished level.

The method copies each array out, so the builder stays usable and a later edit cannot change the level that it just returned. A field that no method touched stays None and does not become an empty array. build_state() therefore separates “not set” from “empty”.

Parameters:

name – Identifier for the level. The method does not test it against LEVELS for uniqueness.

Returns:

A level whose shapes Level.__post_init__ validated.

Return type:

Level

fill_rect(x: int, y: int, w: int, h: int, block: BlockType, resources: int | None = None) → LevelBuilder#

Write a block type across a rectangle, and set its ore if asked.

The method clips the rectangle to the map. A caller can therefore give coordinates that pass an edge, and the method writes the part inside the map. A rectangle fully outside the map writes nothing and still returns self. This is the one placement method that raises nothing on a bad coordinate.

Parameters:
  • x – Left column, included in the rectangle.

  • y – Top row, included in the rectangle.

  • w – Width in tiles. A value of zero or less fills nothing.

  • h – Height in tiles. A value of zero or less fills nothing.

  • block – Block type to write across the region.

  • resources – Ore units for every tile in the region. Without this argument the tiles keep the values that the resource array already held. If no resource array exists, the method creates none, and default_resources() sets the amounts at build time.

Returns:

self, so the calls chain.

Return type:

LevelBuilder

place_machine(x: int, y: int, machine_type: int, direction: int = 0) → LevelBuilder#

Put a machine on a tile, with a facing.

A call on an occupied tile replaces the machine that was there. The block below stays as it was, so this method places a machine on water or on ore, where the placement rules of the engine refuse one.

Parameters:
  • x – Column.

  • y – Row.

  • machine_type – Machine value.

  • direction – Direction value. The default 0 means “no direction”, and a machine that ignores its facing keeps it. A belt or a miner at 0 moves nothing.

Returns:

self, so the calls chain.

Return type:

LevelBuilder

Raises:

IndexError – If the tile is outside the map.

set_machine_inventory(x: int, y: int, item_type: int, count: int) → LevelBuilder#

Give the machine on a tile a starting stock of one item.

The field records the contents by item and holds no slot, so this call gives what a machine holds, and not where. build_state() selects the slot when it builds the entity. For an assembler or a furnace it reads the recipe book to separate a finished output from an input.

The method does not test that a machine stands on the tile. A stock on an empty tile passes through save and load, and the build then drops it.

A machine with one buffer, which is every kind except an assembler and a furnace, holds one item kind. This method accepts a second kind, and build_state() refuses it, because the kind on the tile can change after this call.

Parameters:
  • x – Column.

  • y – Row.

  • item_type – ItemType value.

  • count – Number of items. The method replaces any count for this item on this tile, and does not add to it. It sets no limit, so a value above the stack limit of the machine clamps later.

Returns:

self, so the calls chain.

Return type:

LevelBuilder

Raises:

IndexError – If the tile is outside the map.

set_player_inventory(items: list[tuple[int, int]]) → LevelBuilder#

Give every player the same start inventory.

The method replaces the inventory of an earlier call, and does not add to it. A repeated item inside one list does add up, because build_state() adds the pairs as it reads them.

Parameters:

items – (item, count) pairs. An empty list leaves the players with nothing, and no call to this method has the same result.

Returns:

self, so the calls chain.

Return type:

LevelBuilder

set_player_position(x: int, y: int) → LevelBuilder#

Add one player spawn to the end of the list.

The call order is the player order, so the first call sets player 0. To set the spawn of one player, a caller must also set the spawn of every earlier player. No call removes a spawn.

One spawn commits the level to all of them. build_state() refuses a level whose spawn count does not match the number of players that the caller asked for.

Parameters:
  • x – Column.

  • y – Row.

Returns:

self, so the calls chain.

Return type:

LevelBuilder

Raises:

IndexError – If the position is outside the map.

set_resources(x: int, y: int, amount: int) → LevelBuilder#

Set the ore left in one tile.

This call creates the resource array. A level that calls it one time therefore carries a full array from that point, and build_state() stops taking the amounts from the block map. Every other tile keeps its default value, and ore that a later call paints also gets one, because fill_rect() keeps the array in step.

Parameters:
  • x – Column.

  • y – Row.

  • amount – Ore units to store. The method does not clamp the value, so a value larger than BLOCK_MAX_RESOURCES stands. A tile that is not ore can also take a count that no machine ever mines.

Returns:

self, so the calls chain.

Return type:

LevelBuilder

Raises:

IndexError – If the tile is outside the map.

factoriax.engine.levels.build_state(level: Level, num_players: int, max_machines: int = 0) → EnvState#

Turn a level into a live state, ready to step.

This function turns the per-tile view of the level into the per-entity view of the engine. Each occupied tile gets one entry in the entity arrays, and tile_entity records the join. Everything that the level does not describe starts empty. No craft is in progress, no player mined an item, and no achievement is unlocked.

Where the level leaves a field unset, the default applies, so a plain block map still gives a playable world. Where the level sets a field, the level wins and the function follows it exactly. The mismatches below therefore raise, and the function does not correct them.

Parameters:
  • level – World to build from. The function does not modify it.

  • num_players – Number of players to spawn. This sets the length of every player array. It must match level.player_positions when that field is set.

  • max_machines – Size of the entity table. The default 0 means “select one”, which is max(64, tiles // 4), enough for a quarter of the map. A level cannot place more machines than this number.

Returns:

A state with timestep at 0, and with every player facing DOWN.

Return type:

EnvState

Raises:

ValueError – If the level places more machines than max_machines allows, lists a spawn count other than num_players, or gives a one-buffer machine more than one kind of item. Each case describes a world that the state cannot hold, so the function trims none of them.

Examples

>>> from factoriax.engine.levels import LevelBuilder, build_state
>>> level = LevelBuilder(8, 8).build("tiny")
>>> state = build_state(level, num_players=1)
>>> state.map.shape
(8, 8)
>>> int(state.timestep)
0
factoriax.engine.levels.default_resources(block_map: ndarray) → ndarray#

Fill every ore tile with a full deposit, and every other tile with none.

Ore means every block in factoriax.engine.tables.MINEABLE_BLOCKS. The generated path tests against the same set, so a hand-built level and a generated level agree on which tiles hold something to mine.

Parameters:

block_map – Block type of each tile, shape (H, W).

Returns:

Shape (H, W), int32. An ore tile holds BLOCK_MAX_RESOURCES and every other tile holds 0. This is a new array, and the function does not modify the input.

Return type:

np.ndarray

factoriax.engine.levels.generate_state(rng: Array, params: EnvParams, map_height: int = 32, map_width: int = 32, num_players: int = 1, max_machines: int = 0) → EnvState#

Generate a random world and return its state.

This is a short wrapper for tests and scripts. It builds a FactoriaxEnv and resets it. The result therefore matches what an environment produces, and this function assembles no state of its own. The import happens inside the call, because envs.base imports this module.

JAX cannot trace this function. It builds an environment from Python integers, so it cannot run under jax.jit. Only the reset that it calls is traceable.

Parameters:
  • rng – Key for the terrain draw. The same key gives the same world.

  • params – The function passes this to the reset, which reads the terrain probabilities from it.

  • map_height – Map dimensions in tiles.

  • map_width – Map dimensions in tiles.

  • num_players – Number of players to spawn.

  • max_machines – Size of the entity table. A value of 0 means max(64, tiles // 4).

Returns:

A state on new terrain, with no machines on the map.

Return type:

EnvState

factoriax.engine.levels.generate_terrain(rng: Array, params: EnvParams, map_height: int, map_width: int) → Array#

Draw a random block map of ore patches, water, and dirt.

Each block type gets its own noise field and its own threshold, so the draws are independent. The results then go down in a fixed order, silicon first and water last, and each layer writes over the one before. Only water covers the share that its probability names. Every earlier layer keeps only the tiles that no later layer took, so silicon, the first layer, reaches about half its figure. Dirt covers every tile that no layer took.

Parameters:
  • rng – Key for the draw. The function splits it six ways, one for each block type, so the same key gives the same map.

  • params – The function reads the six *_probability fields. Each one is the share of the map that the block covers on its own, before a later layer writes over it.

  • map_height – Map size in tiles.

  • map_width – Map size in tiles.

Returns:

Block type of each tile, shape (map_height, map_width), int32. The result holds terrain only: no machines, no players, and no ore amounts.

Return type:

jax.Array

factoriax.engine.levels.get_level(name: str) → Level#

Find a built-in level by name.

Parameters:

name – Key into LEVELS.

Returns:

The registered level itself, and not a copy.

CAUTION: Do not write to the arrays of the result. Every caller shares this object with LEVELS, so a write changes the level for the whole process. build_state() only reads it.

Return type:

Level

Raises:

KeyError – If no level carries that name. The message lists the names that exist.

Examples

>>> from factoriax.engine.levels import get_level
>>> get_level("15x15_resources").name
'15x15_resources'
factoriax.engine.levels.initial_state(world_map: Array, params: EnvParams, num_players: int, max_machines: int) → EnvState#

Wrap a generated block map in a state with no machines standing.

This function is the generated counterpart of build_state(). It takes a finished block map and not a Level, so it places nothing. It allocates the entity table, leaves it empty, and the world starts with no machines.

The ore comes from MINEABLE_BLOCKS and params.base_resources. default_resources() uses the same set of blocks but a different amount, so a generated tile and a hand-built tile can hold different amounts of the same ore.

Parameters:
  • world_map – Block type of each tile, shape (H, W). The function writes DIRT to each spawn tile, and the state carries that new copy.

  • params – The function reads base_resources only.

  • num_players – Number of players to spawn. They stand along the middle row, as in _place_players().

  • max_machines – Size of the entity table. A value of 0 means max(64, tiles // 4).

Returns:

A state with an empty entity table and timestep at 0. Every player faces DOWN and holds an empty inventory.

Return type:

EnvState

factoriax.engine.levels.load_level(path: Path) → Level#

Read a Level from a JSON file that save_level() wrote.

Only name, map_width, map_height, and block_map are required. Every other field can hold null or be absent, and both mean “not set”. A hand-written file therefore holds only the fields that it needs.

An array comes back as int32 and a position list as tuples, which is what LevelBuilder produces. A level therefore passes through a save and a load unchanged. Level.__post_init__ tests the shapes, so a file whose arrays disagree with its dimensions fails here.

Parameters:

path – File to read.

Returns:

The level that the file describes.

Return type:

Level

Raises:
  • KeyError – If a required field is absent.

  • ValueError – If an array disagrees with the declared dimensions.

Examples

>>> import tempfile
>>> from pathlib import Path
>>> from factoriax.engine.levels import LevelBuilder, load_level, save_level
>>> with tempfile.TemporaryDirectory() as directory:
...     path = Path(directory) / "tiny.json"
...     save_level(LevelBuilder(8, 8).build("tiny"), path)
...     load_level(path).name
'tiny'
factoriax.engine.levels.save_level(level: Level, path: Path) → None#

Write a level to a JSON file.

Each array becomes a nested list of integers, and the output is indented. A person can therefore read and edit a level by hand. The function writes every optional field, and writes null where a field is unset. load_level() also accepts a file that leaves a field out.

The format carries no version marker. A later change to the field set therefore has no way to announce itself to an older reader.

Parameters:
  • level – Level to write. The function does not modify it.

  • path – Destination file. The function creates the parent directories, and writes over an existing file with no warning.

Examples

>>> import tempfile
>>> from pathlib import Path
>>> from factoriax.engine.levels import LevelBuilder, save_level
>>> level = LevelBuilder(8, 8).build("tiny")
>>> with tempfile.TemporaryDirectory() as directory:
...     path = Path(directory) / "tiny.json"
...     save_level(level, path)
...     path.exists()
True