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
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_mapis optional. A field that holdsNonemeans “letbuild_state()decide”, and not “empty”. With noblock_resources,default_resources()fills the ore tiles. With noplayer_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], soblock_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:
- map_width, map_height
Tile dimensions. The class validates every array field against them.
- block_map#
BlockTypeof each tile. Shape(map_height, map_width). This is the one required field.- Type:
- block_resources#
Ore units in each tile, or
Noneto take them fromblock_map.- Type:
numpy.ndarray | None
- machine_types#
Machineon each tile,NONEwhere the tile is clear, orNonefor 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, sobuild_state()must decide which slot holds each item.- Type:
numpy.ndarray | None
- player_inventories#
Contents for one player, with the player index as the key. For a player that it names, it replaces
player_inventoryand does not add to it. An index past the player count has no effect.
- 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.
- Raises:
ValueError – If an array field disagrees with
map_widthormap_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
Levela tile at a time.Every method that writes returns
self, so the calls chain, andbuild()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 staysNonein the finished level, andbuild_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
Noneand does not become an empty array.build_state()therefore separates “not set” from “empty”.
- 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:
- 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:
- Returns:
self, so the calls chain.- Return type:
- 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 –
ItemTypevalue.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:
- 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:
- 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:
- 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, becausefill_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_RESOURCESstands. A tile that is not ore can also take a count that no machine ever mines.
- Returns:
self, so the calls chain.- Return type:
- 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_entityrecords 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_positionswhen 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
timestepat 0, and with every player facing DOWN.- Return type:
- Raises:
ValueError – If the level places more machines than
max_machinesallows, lists a spawn count other thannum_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 holdsBLOCK_MAX_RESOURCESand 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
FactoriaxEnvand 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, becauseenvs.baseimports 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:
- 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
*_probabilityfields. 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:
- 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:
- 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 aLevel, so it places nothing. It allocates the entity table, leaves it empty, and the world starts with no machines.The ore comes from
MINEABLE_BLOCKSandparams.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_resourcesonly.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
timestepat 0. Every player faces DOWN and holds an empty inventory.- Return type:
- factoriax.engine.levels.load_level(path: Path) Level#
Read a
Levelfrom a JSON file thatsave_level()wrote.Only
name,map_width,map_height, andblock_mapare required. Every other field can holdnullor 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
LevelBuilderproduces. 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:
- 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
nullwhere 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