factoriax.engine.envs.miner_curriculum#

MinerBootstrap-v1: a sparse task that pays only on completion.

The player starts with an empty inventory on the 16x16 six-patch terrain of factoriax.engine.envs.common. The task is to mine limestone and silicon, craft six miners, and put all six into production. The reward is 1.0 exactly one time in an episode, on the step that completes the task. That step also ends the episode. Every other step pays 0, and the highest score is 1.

The ladder of 14 gate bits over the mine, craft, and place loop stays as a zero-weight diagnostic. The bits latch through achievement_hook(), so analysis code can see how far an episode reached. They pay nothing, and they are not part of the observation, so the agent sees no difference.

This sparsity is deliberate. Standard PPO from scratch is expected to fail here, and that failure is the experimental baseline. The hypothesis under test is that a backward curriculum over start states solves what sparse-reward PPO cannot. Training starts from states that are almost done, where the agent places one last miner, and walks backwards to the empty-inventory start. An earlier forward curriculum used three separate staged environments: MineOres-v1, CraftMiners-v1, and PlaceMiners-v1. The git history holds them. That curriculum lost against PPO from scratch or drew with it, so it is gone.

This scenario shares the world and the recipe table of EasyRocket-v1. It defaults to the same egocentric superficial_local observation with radius 7. A policy that trains here therefore transfers to an EasyRocket-v1 built with the same observation arguments, with no change.

Module Attributes

MINER_BOOTSTRAP_ACHIEVEMENTS

14 latched bits over the whole mine, craft, and place loop.

MINER_BOOTSTRAP_MAX_SCORE

one episode pays at most 1.0.

class factoriax.engine.envs.miner_curriculum.BootstrapStart(n_placed: int, n_miners: int, n_materials: int, partial_mine: bool = False)#

Start-state spec for one stage of the backward curriculum.

The task divides into mine, then craft, then place. The ladder undoes it in the reverse order. With k missing miners, from 1 to 6, place(k), or A_k, starts one group of actions from the end. craft(k), or B_k, starts two groups from the end, and mine(k), or C_k, starts three groups from the end. mine(6) is the full task. Every count is a Python static that the environment holds from build time.

classmethod craft(k: int) → BootstrapStart#

B_k: 6-k produce, k limestone and k silicon. Craft and place.

classmethod mine(k: int) → BootstrapStart#

C_k: 6-k produce, empty inventory. Mine, craft, and place.

classmethod mine_partial(k: int) → BootstrapStart#

B’_k: 6-k produce, k of one ore. Mine the other ore, craft, and place.

The ore that the player holds is limestone or silicon, and each episode draws it with equal probability. The policy must therefore learn to mine both.

classmethod place(k: int) → BootstrapStart#

A_k: 6-k miners produce, k miners in the inventory. Walk and place.

factoriax.engine.envs.miner_curriculum.MINER_BOOTSTRAP_ACHIEVEMENTS: tuple[Achievement, ...] = (Achievement(id='mine_limestone', condition=functools.partial(<function mined_at_least>, item=6, count=1), name='Mine Limestone', hint='', weight=0.0), Achievement(id='limestone_stocked', condition=functools.partial(<function mined_at_least>, item=6, count=6), name='Limestone Stocked', hint='', weight=0.0), Achievement(id='mine_silicon', condition=functools.partial(<function mined_at_least>, item=5, count=1), name='Mine Silicon', hint='', weight=0.0), Achievement(id='silicon_stocked', condition=functools.partial(<function mined_at_least>, item=5, count=6), name='Silicon Stocked', hint='', weight=0.0), Achievement(id='craft_miner', condition=functools.partial(<function holds_item>, item=24), name='Craft Miner', hint='', weight=0.0), Achievement(id='place_1_miners', condition=functools.partial(<function _placed_at_least>, count=1), name='Place 1 Miners', hint='', weight=0.0), Achievement(id='place_3_miners', condition=functools.partial(<function _placed_at_least>, count=3), name='Place 3 Miners', hint='', weight=0.0), Achievement(id='place_6_miners', condition=functools.partial(<function _placed_at_least>, count=6), name='Place 6 Miners', hint='', weight=0.0), Achievement(id='producing_1_miners', condition=functools.partial(<function _producing_at_least>, count=1), name='1 Miners Producing', hint='', weight=0.0), Achievement(id='producing_2_miners', condition=functools.partial(<function _producing_at_least>, count=2), name='2 Miners Producing', hint='', weight=0.0), Achievement(id='producing_3_miners', condition=functools.partial(<function _producing_at_least>, count=3), name='3 Miners Producing', hint='', weight=0.0), Achievement(id='producing_4_miners', condition=functools.partial(<function _producing_at_least>, count=4), name='4 Miners Producing', hint='', weight=0.0), Achievement(id='producing_5_miners', condition=functools.partial(<function _producing_at_least>, count=5), name='5 Miners Producing', hint='', weight=0.0), Achievement(id='producing_6_miners', condition=functools.partial(<function _producing_at_least>, count=6), name='6 Miners Producing', hint='', weight=1.0))#

14 latched bits over the whole mine, craft, and place loop. Only the last bit pays. The other 13 carry weight 0 and serve as a diagnostic for analysis: how far did an episode reach before it stopped. The bits are:

  • 4 mining bits: 1 and 6 of each miner ingredient, on the items_mined counter, which never decreases. 6 of each is the material for six miners.

  • 1 craft bit: the player holds a miner.

  • 3 placement bits: 1, 3, and 6 miners placed.

  • 6 production bits: 1 to 6 producing miners. The last one is the task.

factoriax.engine.envs.miner_curriculum.MINER_BOOTSTRAP_MAX_SCORE: float = 1.0#

one episode pays at most 1.0.

Type:

Completion-only

factoriax.engine.envs.miner_curriculum.all_miners_producing(state: EnvState, params: EnvParams) → Array#

End condition of an episode: six miners produce at the same time.

factoriax.engine.envs.miner_curriculum.apply_start(key: Array, state: EnvState, params: EnvParams, start: BootstrapStart) → EnvState#

Turn a plain reset state into the stage state that start describes.

The function is pure and jittable. miner_bootstrap() uses it as a reset hook, and analysis code can import it on its own.

It places start.n_placed miners, one on each ore patch. key selects the patches, so the ore type of the free patch changes with the episode. Each placement goes through place_machine(), and the function moves the player above each patch corner for that one call. The new entity therefore matches a placement from the action path, field for field.

The function then puts one unit of the ore of its tile into the output buffer of each new miner, so the miner counts as a producer from step 0. Last, it puts the player back where it was and sets the inventory to the counts of the stage.

factoriax.engine.envs.miner_curriculum.miner_bootstrap(*, start: BootstrapStart | None = None, obs: str = 'superficial_local', obs_radius: int = 7) → tuple[FactoriaxEnv, EnvParams]#

Build the MinerBootstrap-v1 environment.

The player starts with an empty inventory. The task is to mine limestone and silicon, craft six miners, and put all six into production on ore. Completion pays 1.0 and ends the episode. Every other step pays 0, and the highest score is 1.

Parameters:
  • start – Stage state of the backward curriculum to reset into. None builds the full task, which equals BootstrapStart.mine(6). The world, the observation, the actions, the reward, and the step budget are the same for every stage. Only the start state changes.

  • obs – Observation variant. The function passes it to FactoriaxEnv.

  • obs_radius – Half-width of the egocentric local window. The default radius 7 gives a 15x15 view on the 16x16 map. A _global variant ignores it.

factoriax.engine.envs.miner_curriculum.miner_bootstrap_reward(prev_state: EnvState, new_state: EnvState, params: EnvParams) → Array#

Sparse reward: 1.0 on the step where the completion bit latches, else 0.