├── tests
├── __init__.py
├── examples
│ ├── __init__.py
│ ├── test_gym_make.py
│ ├── minecraft
│ │ ├── __init__.py
│ │ ├── test_render.py
│ │ └── test_requirements_graph.py
│ ├── minigrid
│ │ ├── __init__.py
│ │ └── test_fourrooms.py
│ ├── test_random.py
│ ├── test_tower.py
│ └── test_recursive.py
├── planning
│ ├── __init__.py
│ ├── test_dummy_env.py
│ ├── test_planning.py
│ ├── test_can_solve.py
│ └── test_mineHcraft.py
├── render
│ ├── __init__.py
│ ├── test_widgets.py
│ └── test_render.py
├── requirements
│ ├── __init__.py
│ ├── test_can_draw.py
│ └── test_available_from_start.py
├── solving_behaviors
│ ├── __init__.py
│ ├── test_doc_example.py
│ ├── test_can_solve.py
│ └── test_mineHcraft.py
├── test_build_env.py
├── test_random_legal.py
├── test_render.py
├── custom_checks.py
├── test_world.py
├── test_cli.py
├── envs.py
└── test_tasks.py
├── MANIFEST.in
├── requirements.txt
├── src
└── hcraft
│ ├── render
│ ├── __init__.py
│ ├── default_resources
│ │ └── font.ttf
│ └── human.py
│ ├── behaviors
│ ├── __init__.py
│ ├── actions.py
│ ├── utils.py
│ └── feature_conditions.py
│ ├── examples
│ ├── minecraft
│ │ ├── resources
│ │ │ ├── font.ttf
│ │ │ ├── items
│ │ │ │ ├── book.png
│ │ │ │ ├── coal.png
│ │ │ │ ├── dirt.png
│ │ │ │ ├── egg.png
│ │ │ │ ├── wood.png
│ │ │ │ ├── clock.png
│ │ │ │ ├── flint.png
│ │ │ │ ├── gravel.png
│ │ │ │ ├── paper.png
│ │ │ │ ├── reeds.png
│ │ │ │ ├── stick.png
│ │ │ │ ├── blaze_rod.png
│ │ │ │ ├── diamond.png
│ │ │ │ ├── ender_eye.png
│ │ │ │ ├── furnace.png
│ │ │ │ ├── gold_axe.png
│ │ │ │ ├── gold_ore.png
│ │ │ │ ├── iron_axe.png
│ │ │ │ ├── iron_ore.png
│ │ │ │ ├── leather.png
│ │ │ │ ├── obsidian.png
│ │ │ │ ├── redstone.png
│ │ │ │ ├── stone_axe.png
│ │ │ │ ├── wood_axe.png
│ │ │ │ ├── cobblestone.png
│ │ │ │ ├── diamond_axe.png
│ │ │ │ ├── ender_pearl.png
│ │ │ │ ├── gold_ingot.png
│ │ │ │ ├── gold_shovel.png
│ │ │ │ ├── gold_sword.png
│ │ │ │ ├── iron_ingot.png
│ │ │ │ ├── iron_shovel.png
│ │ │ │ ├── iron_sword.png
│ │ │ │ ├── netherrack.png
│ │ │ │ ├── stone_sword.png
│ │ │ │ ├── wood_plank.png
│ │ │ │ ├── wood_shovel.png
│ │ │ │ ├── wood_sword.png
│ │ │ │ ├── blaze_powder.png
│ │ │ │ ├── crafting_table.png
│ │ │ │ ├── diamond_shovel.png
│ │ │ │ ├── diamond_sword.png
│ │ │ │ ├── gold_pickaxe.png
│ │ │ │ ├── iron_pickaxe.png
│ │ │ │ ├── stone_pickaxe.png
│ │ │ │ ├── stone_shovel.png
│ │ │ │ ├── wood_pickaxe.png
│ │ │ │ ├── diamond_pickaxe.png
│ │ │ │ ├── enchanting_table.png
│ │ │ │ ├── flint_and_steel.png
│ │ │ │ ├── close_ender_portal.png
│ │ │ │ ├── close_nether_portal.png
│ │ │ │ ├── ender_dragon_head.png
│ │ │ │ ├── open_ender_portal.png
│ │ │ │ └── open_nether_portal.png
│ │ │ └── zones
│ │ │ │ ├── end.png
│ │ │ │ ├── forest.png
│ │ │ │ ├── meadow.png
│ │ │ │ ├── nether.png
│ │ │ │ ├── swamp.png
│ │ │ │ ├── bedrock.png
│ │ │ │ ├── stronghold.png
│ │ │ │ └── underground.png
│ │ ├── zones.py
│ │ ├── tools.py
│ │ ├── env.py
│ │ └── __init__.py
│ ├── minicraft
│ │ ├── resources
│ │ │ ├── font.ttf
│ │ │ └── items
│ │ │ │ ├── ball.png
│ │ │ │ ├── box.png
│ │ │ │ ├── goal.png
│ │ │ │ ├── key.png
│ │ │ │ ├── lava.png
│ │ │ │ ├── weight.png
│ │ │ │ ├── open_door.png
│ │ │ │ ├── closed_door.png
│ │ │ │ ├── locked_door.png
│ │ │ │ ├── blocked_door.png
│ │ │ │ ├── blue_open_door.png
│ │ │ │ ├── blue_closed_door.png
│ │ │ │ └── blocked_locked_door.png
│ │ ├── empty.py
│ │ ├── crossing.py
│ │ ├── multiroom.py
│ │ ├── fourrooms.py
│ │ ├── unlock.py
│ │ ├── minicraft.py
│ │ ├── __init__.py
│ │ ├── doorkey.py
│ │ ├── unlockpickup.py
│ │ ├── keycorridor.py
│ │ └── unlockpickupblocked.py
│ ├── treasure
│ │ ├── resources
│ │ │ └── items
│ │ │ │ ├── gold.png
│ │ │ │ ├── key.png
│ │ │ │ ├── locked_chest.png
│ │ │ │ └── treasure_chest.png
│ │ ├── __init__.py
│ │ └── env.py
│ ├── random_simple
│ │ ├── __init__.py
│ │ └── env.py
│ ├── recursive.py
│ ├── light_recursive.py
│ ├── tower.py
│ └── __init__.py
│ ├── __main__.py
│ ├── elements.py
│ ├── metrics.py
│ ├── __init__.py
│ ├── solving_behaviors.py
│ └── task.py
├── commands
├── generate_paper.ps1
└── generate_html_doc.ps1
├── docs
├── images
│ ├── TreasureEnvV1.png
│ ├── TreasureEnvV2.png
│ ├── hcraft_state.png
│ ├── hcraft_observation.png
│ ├── MinigridHierarchies.png
│ ├── HierarchyCraft_pipeline.png
│ ├── hcraft_transformation.png
│ ├── minehcraft_human_demo.gif
│ ├── CrafterRequirementsGraph.png
│ ├── HierarchyCraftStateLarge.png
│ ├── MineHcraft_face_to_dragon.png
│ ├── HCraftGUI_MineHCraft_Dragon.png
│ ├── PDDL_HierarchyCraft_domain.png
│ ├── PDDL_HierarchyCraft_problem.png
│ ├── TransformationToRequirements.png
│ ├── HierachyCraft_domain_position.png
│ ├── requirements_graphs
│ │ ├── MineHCraft.png
│ │ ├── MiniHCraftEmpty.png
│ │ ├── TreasureHcraft.png
│ │ ├── MiniHCraftCrossing.png
│ │ ├── MiniHCraftDoorKey.png
│ │ ├── MiniHCraftUnlock.png
│ │ ├── RecursiveHcraft-I6.png
│ │ ├── TowerHcraft-H2-W3.png
│ │ ├── MiniHCraftFourRooms.png
│ │ ├── MiniHCraftMultiRoom.png
│ │ ├── MiniHCraftKeyCorridor.png
│ │ ├── MiniHCraftUnlockPickup.png
│ │ ├── LightRecursiveHcraft-K2-I6.png
│ │ └── MiniHCraftBlockedUnlockPickup.png
│ ├── TransformationToRequirementsLarge.png
│ └── MineRLCompetitionRequirementsGraph.png
└── template
│ ├── custom.css
│ ├── livereload.html.jinja2
│ ├── theme.css
│ ├── build-search-index.js
│ ├── math.html.jinja2
│ ├── README.md
│ ├── search.js.jinja2
│ └── layout.css
├── setup.py
├── .coveragerc
├── .github
└── workflows
│ ├── draft-pdf.yml
│ ├── python-pypi.yml
│ ├── python-tests-no-optdeps.yml
│ ├── python-coverage.yml
│ ├── python-tests-all-optdeps.yml
│ └── pydoc-github-pages.yml
├── .pre-commit-config.yaml
├── shell.nix
├── CONTRIBUTING.md
├── flake.nix
├── flake.lock
├── .gitignore
└── pyproject.toml
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/examples/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/planning/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/render/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/examples/test_gym_make.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/requirements/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/examples/minecraft/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/examples/minigrid/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/solving_behaviors/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | graft hcraft/examples/**/resources
2 | graft hcraft/render/default_resources
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy
2 | networkx >= 2.5.1
3 | matplotlib
4 | seaborn
5 | hebg >= 0.2.3
6 |
--------------------------------------------------------------------------------
/src/hcraft/render/__init__.py:
--------------------------------------------------------------------------------
1 | """Render : Module for rendering HierarchyCraft environments."""
2 |
--------------------------------------------------------------------------------
/commands/generate_paper.ps1:
--------------------------------------------------------------------------------
1 | docker run --rm --volume $PWD/:/data --env JOURNAL=joss openjournals/inara
2 |
--------------------------------------------------------------------------------
/src/hcraft/behaviors/__init__.py:
--------------------------------------------------------------------------------
1 | """Module to define HEBGraphs for any HierarchyCraft environment."""
2 |
--------------------------------------------------------------------------------
/docs/images/TreasureEnvV1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/TreasureEnvV1.png
--------------------------------------------------------------------------------
/docs/images/TreasureEnvV2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/TreasureEnvV2.png
--------------------------------------------------------------------------------
/docs/images/hcraft_state.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/hcraft_state.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | # For retrocompatibility with pip < 22 in editable mode
4 | setup()
5 |
--------------------------------------------------------------------------------
/docs/images/hcraft_observation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/hcraft_observation.png
--------------------------------------------------------------------------------
/docs/images/MinigridHierarchies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/MinigridHierarchies.png
--------------------------------------------------------------------------------
/docs/images/HierarchyCraft_pipeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/HierarchyCraft_pipeline.png
--------------------------------------------------------------------------------
/docs/images/hcraft_transformation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/hcraft_transformation.png
--------------------------------------------------------------------------------
/docs/images/minehcraft_human_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/minehcraft_human_demo.gif
--------------------------------------------------------------------------------
/docs/images/CrafterRequirementsGraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/CrafterRequirementsGraph.png
--------------------------------------------------------------------------------
/docs/images/HierarchyCraftStateLarge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/HierarchyCraftStateLarge.png
--------------------------------------------------------------------------------
/docs/images/MineHcraft_face_to_dragon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/MineHcraft_face_to_dragon.png
--------------------------------------------------------------------------------
/docs/images/HCraftGUI_MineHCraft_Dragon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/HCraftGUI_MineHCraft_Dragon.png
--------------------------------------------------------------------------------
/docs/images/PDDL_HierarchyCraft_domain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/PDDL_HierarchyCraft_domain.png
--------------------------------------------------------------------------------
/docs/images/PDDL_HierarchyCraft_problem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/PDDL_HierarchyCraft_problem.png
--------------------------------------------------------------------------------
/docs/images/TransformationToRequirements.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/TransformationToRequirements.png
--------------------------------------------------------------------------------
/src/hcraft/render/default_resources/font.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/render/default_resources/font.ttf
--------------------------------------------------------------------------------
/docs/images/HierachyCraft_domain_position.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/HierachyCraft_domain_position.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MineHCraft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MineHCraft.png
--------------------------------------------------------------------------------
/docs/images/TransformationToRequirementsLarge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/TransformationToRequirementsLarge.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/font.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/font.ttf
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/font.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/font.ttf
--------------------------------------------------------------------------------
/docs/images/MineRLCompetitionRequirementsGraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/MineRLCompetitionRequirementsGraph.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftEmpty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftEmpty.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/TreasureHcraft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/TreasureHcraft.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftCrossing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftCrossing.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftDoorKey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftDoorKey.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftUnlock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftUnlock.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/RecursiveHcraft-I6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/RecursiveHcraft-I6.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/TowerHcraft-H2-W3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/TowerHcraft-H2-W3.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/book.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/book.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/coal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/coal.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/dirt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/dirt.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/egg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/egg.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/wood.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/wood.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/end.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/ball.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/box.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/goal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/goal.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/key.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/lava.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/lava.png
--------------------------------------------------------------------------------
/src/hcraft/examples/treasure/resources/items/gold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/treasure/resources/items/gold.png
--------------------------------------------------------------------------------
/src/hcraft/examples/treasure/resources/items/key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/treasure/resources/items/key.png
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | show_missing = True
3 | omit =
4 | hcraft/examples/minecraft/*
5 | exclude_lines =
6 | pragma: no cover
7 | if TYPE_CHECKING:
8 |
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftFourRooms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftFourRooms.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftMultiRoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftMultiRoom.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/clock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/clock.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/flint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/flint.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/gravel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/gravel.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/paper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/paper.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/reeds.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/reeds.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/stick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/stick.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/forest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/forest.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/meadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/meadow.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/nether.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/nether.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/swamp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/swamp.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/weight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/weight.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftKeyCorridor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftKeyCorridor.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftUnlockPickup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftUnlockPickup.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/blaze_rod.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/blaze_rod.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/diamond.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/diamond.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/ender_eye.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/ender_eye.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/furnace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/furnace.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/gold_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/gold_axe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/gold_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/gold_ore.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/iron_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/iron_axe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/iron_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/iron_ore.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/leather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/leather.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/obsidian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/obsidian.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/redstone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/redstone.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/stone_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/stone_axe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/wood_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/wood_axe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/bedrock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/bedrock.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/open_door.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/open_door.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/cobblestone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/cobblestone.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/diamond_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/diamond_axe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/ender_pearl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/ender_pearl.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/gold_ingot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/gold_ingot.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/gold_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/gold_shovel.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/gold_sword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/gold_sword.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/iron_ingot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/iron_ingot.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/iron_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/iron_shovel.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/iron_sword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/iron_sword.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/netherrack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/netherrack.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/stone_sword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/stone_sword.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/wood_plank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/wood_plank.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/wood_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/wood_shovel.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/wood_sword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/wood_sword.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/stronghold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/stronghold.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/zones/underground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/zones/underground.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/closed_door.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/closed_door.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/locked_door.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/locked_door.png
--------------------------------------------------------------------------------
/src/hcraft/examples/treasure/resources/items/locked_chest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/treasure/resources/items/locked_chest.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/LightRecursiveHcraft-K2-I6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/LightRecursiveHcraft-K2-I6.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/blaze_powder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/blaze_powder.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/crafting_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/crafting_table.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/diamond_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/diamond_shovel.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/diamond_sword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/diamond_sword.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/gold_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/gold_pickaxe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/iron_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/iron_pickaxe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/stone_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/stone_pickaxe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/stone_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/stone_shovel.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/wood_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/wood_pickaxe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/blocked_door.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/blocked_door.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/blue_open_door.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/blue_open_door.png
--------------------------------------------------------------------------------
/src/hcraft/examples/treasure/resources/items/treasure_chest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/treasure/resources/items/treasure_chest.png
--------------------------------------------------------------------------------
/docs/images/requirements_graphs/MiniHCraftBlockedUnlockPickup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/docs/images/requirements_graphs/MiniHCraftBlockedUnlockPickup.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/diamond_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/diamond_pickaxe.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/enchanting_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/enchanting_table.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/flint_and_steel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/flint_and_steel.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/blue_closed_door.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/blue_closed_door.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/close_ender_portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/close_ender_portal.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/close_nether_portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/close_nether_portal.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/ender_dragon_head.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/ender_dragon_head.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/open_ender_portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/open_ender_portal.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/resources/items/open_nether_portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minecraft/resources/items/open_nether_portal.png
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/resources/items/blocked_locked_door.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IRLL/HierarchyCraft/HEAD/src/hcraft/examples/minicraft/resources/items/blocked_locked_door.png
--------------------------------------------------------------------------------
/commands/generate_html_doc.ps1:
--------------------------------------------------------------------------------
1 | pdoc -d google -t docs/template --logo https://irll.net/irll-logo.png --logo-link https://irll.github.io/HierarchyCraft/hcraft.html -o docs/build --math --no-search hcraft
2 |
--------------------------------------------------------------------------------
/docs/template/custom.css:
--------------------------------------------------------------------------------
1 | .pdoc h1, .pdoc h2, .pdoc h3 {
2 | font-weight: 500;
3 | }
4 |
5 | h1 {
6 | color: #006a2f;
7 | }
8 |
9 | h2 {
10 | color: #6ca47e;
11 | }
12 |
13 | h3 {
14 | color: #aeccb6;
15 | }
16 |
--------------------------------------------------------------------------------
/src/hcraft/__main__.py:
--------------------------------------------------------------------------------
1 | from hcraft.cli import hcraft_cli
2 | from hcraft.render.human import render_env_with_human
3 |
4 |
5 | def main():
6 | """Run hcraftommand line interface."""
7 | env = hcraft_cli()
8 | render_env_with_human(env)
9 |
10 |
11 | if __name__ == "__main__":
12 | main()
13 |
--------------------------------------------------------------------------------
/tests/examples/minigrid/test_fourrooms.py:
--------------------------------------------------------------------------------
1 | from hcraft.examples.minicraft.fourrooms import _get_rooms_connections
2 |
3 |
4 | def test_four_rooms_should_be_rightfully_connected():
5 | assert _get_rooms_connections([0, 1, 2, 3]) == {
6 | 0: [3, 1],
7 | 1: [0, 2],
8 | 2: [1, 3],
9 | 3: [2, 0],
10 | }
11 |
--------------------------------------------------------------------------------
/tests/test_build_env.py:
--------------------------------------------------------------------------------
1 | from hcraft.examples import EXAMPLE_ENVS
2 | from hcraft.render.human import render_env_with_human
3 |
4 |
5 | import pytest
6 |
7 |
8 | @pytest.mark.slow
9 | @pytest.mark.parametrize("env_class", EXAMPLE_ENVS)
10 | def test_build_env(env_class):
11 | human_run = False
12 | env = env_class()
13 | if human_run:
14 | render_env_with_human(env)
15 |
--------------------------------------------------------------------------------
/tests/examples/minecraft/test_render.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from hcraft.examples.minecraft.env import MineHcraftEnv
3 | from hcraft.render.utils import build_transformation_image
4 |
5 |
6 | @pytest.mark.slow
7 | def test_render_transformation():
8 | env = MineHcraftEnv()
9 | for transfo in env.world.transformations:
10 | build_transformation_image(transfo, env.world.resources_path)
11 |
--------------------------------------------------------------------------------
/src/hcraft/examples/random_simple/__init__.py:
--------------------------------------------------------------------------------
1 | from hcraft.examples.random_simple.env import RandomHcraftEnv
2 |
3 | __all__ = ["RandomHcraftEnv"]
4 |
5 | # gym is an optional dependency
6 | try:
7 | import gymnasium as gym
8 |
9 | gym.register(
10 | id="RandomHcraft-v1",
11 | entry_point="hcraft.examples.random_simple.env:RandomHcraftEnv",
12 | )
13 | except ImportError:
14 | pass
15 |
--------------------------------------------------------------------------------
/.github/workflows/draft-pdf.yml:
--------------------------------------------------------------------------------
1 | name: Paper Draft
2 | on: [push]
3 |
4 | jobs:
5 | paper:
6 | runs-on: ubuntu-latest
7 | name: Paper Draft
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v3
11 | - name: Build draft PDF
12 | uses: openjournals/openjournals-draft-action@master
13 | with:
14 | journal: joss
15 | paper-path: paper.md
16 | - name: Upload
17 | uses: actions/upload-artifact@v4
18 | with:
19 | name: paper
20 | path: paper.pdf
21 |
--------------------------------------------------------------------------------
/docs/template/livereload.html.jinja2:
--------------------------------------------------------------------------------
1 | {# This templates implements live-reloading for pdoc's integrated webserver. #}
2 |
16 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/zones.py:
--------------------------------------------------------------------------------
1 | """Minecraft Zones
2 |
3 | Abstract zones to simulate simply a Minecraft environment.
4 |
5 | """
6 |
7 | from hcraft.elements import Zone
8 |
9 | #: Zones
10 | FOREST = Zone("forest") #: FOREST
11 | SWAMP = Zone("swamp") #: SWAMP
12 | MEADOW = Zone("meadow") #: MEADOW
13 | UNDERGROUND = Zone("underground") #: UNDERGROUND
14 | BEDROCK = Zone("bedrock") #: BEDROCK
15 | NETHER = Zone("nether") #: NETHER
16 | END = Zone("end") #: ENDER
17 | STRONGHOLD = Zone("stronghold") #: STRONGHOLD
18 |
19 | OVERWORLD = [FOREST, SWAMP, MEADOW, UNDERGROUND, BEDROCK, STRONGHOLD]
20 |
21 | MC_ZONES = OVERWORLD + [NETHER, END]
22 |
--------------------------------------------------------------------------------
/src/hcraft/elements.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass(frozen=True)
5 | class Item:
6 | """Represent an item for any hcraft environement."""
7 |
8 | name: str
9 |
10 |
11 | @dataclass(frozen=True)
12 | class Stack:
13 | """Represent a stack of an item for any hcraft environement"""
14 |
15 | item: Item
16 | quantity: int = 1
17 |
18 | def __str__(self) -> str:
19 | quantity_str = f"[{self.quantity}]" if self.quantity > 1 else ""
20 | return f"{quantity_str}{self.item.name}"
21 |
22 |
23 | @dataclass(frozen=True)
24 | class Zone:
25 | """Represent a zone for any hcraft environement."""
26 |
27 | name: str
28 |
--------------------------------------------------------------------------------
/src/hcraft/examples/treasure/__init__.py:
--------------------------------------------------------------------------------
1 | """A simple environment used in for the env building tutorial:
2 | [`hcraft.env`](https://irll.github.io/HierarchyCraft/hcraft/env.html)
3 |
4 | Requirements graph:
5 |
6 | .. include:: ../../../../docs/images/requirements_graphs/TreasureHcraft.html
7 |
8 |
9 | """
10 |
11 | from hcraft.examples.treasure.env import TreasureEnv
12 |
13 | __all__ = ["TreasureEnv"]
14 |
15 | # gym is an optional dependency
16 | try:
17 | import gymnasium as gym
18 |
19 | gym.register(
20 | id="Treasure-v1",
21 | entry_point="hcraft.examples.treasure.env:TreasureEnv",
22 | )
23 |
24 |
25 | except ImportError:
26 | pass
27 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v2.3.0
4 | hooks:
5 | - id: check-yaml
6 | - id: end-of-file-fixer
7 | - id: trailing-whitespace
8 | - repo: https://github.com/astral-sh/ruff-pre-commit
9 | rev: v0.1.13
10 | hooks:
11 | - id: ruff
12 | types_or: [ python, pyi, jupyter ]
13 | args: [ --fix ]
14 | - id: ruff-format
15 | types_or: [ python, pyi, jupyter ]
16 | - repo: local
17 | hooks:
18 | - id: pytest-check
19 | name: pytest-check
20 | entry: pytest tests
21 | stages: ["pre-push"]
22 | language: system
23 | pass_filenames: false
24 | always_run: true
25 |
--------------------------------------------------------------------------------
/docs/template/theme.css:
--------------------------------------------------------------------------------
1 | /* pdoc dark-mode color scheme */
2 | :root {
3 | --pdoc-background: #ffffff;
4 | }
5 |
6 | .pdoc {
7 | --text: hsl(0, 0%, 0%);
8 | --muted: hsl(0, 0%, 47%);
9 | --link: hsl(212, 100%, 67%);
10 | --link-hover: hsl(216, 100%, 61%);
11 |
12 | --active: hsl(0, 0%, 91%);
13 |
14 | --code: hsl(215, 21%, 95%);
15 | --code-text: hsl(0, 0%, 7%);
16 |
17 | --accent: hsl(0, 0%, 7%);
18 | --accent2: hsl(0, 0%, 27%);
19 |
20 | --nav-text: #aeccb6;
21 | --nav-background: hsl(0, 0%, 7%);
22 | --nav-hover: hsl(0, 0%, 12%);
23 | --nav-border: hsl(0, 0%, 27%);
24 |
25 | --name: hsl(207, 38%, 43%);
26 | --def: hsl(147, 100%, 21%);
27 | --annotation: hsl(137, 100%, 38%);
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/python-pypi.yml:
--------------------------------------------------------------------------------
1 | name: PyPi
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | deploy:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Set up Python
16 | uses: actions/setup-python@v1
17 | with:
18 | python-version: '3.x'
19 | - name: Install dependencies
20 | run: |
21 | python -m pip install --upgrade pip
22 | pip install build twine
23 | - name: Build
24 | run: |
25 | python -m build
26 | - name: Publish
27 | env:
28 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
30 | run: |
31 | twine upload dist/*
32 |
--------------------------------------------------------------------------------
/tests/test_random_legal.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest_check as check
3 |
4 | from hcraft.env import HcraftEnv
5 | from tests.envs import classic_env
6 |
7 |
8 | def random_legal_agent(observation, action_is_legal):
9 | action = np.random.choice(np.nonzero(action_is_legal)[0])
10 | return int(action)
11 |
12 |
13 | def test_random_legal_agent():
14 | world = classic_env()[1]
15 | env = HcraftEnv(world, max_step=10)
16 | done = False
17 | observation, _info = env.reset()
18 | total_reward = 0
19 | while not done:
20 | action_is_legal = env.action_masks()
21 | action = random_legal_agent(observation, action_is_legal)
22 | _observation, reward, terminated, truncated, _info = env.step(action)
23 | done = terminated or truncated
24 | total_reward += reward
25 |
26 | check.greater_equal(total_reward, 0)
27 |
--------------------------------------------------------------------------------
/tests/test_render.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from hcraft.env import HcraftEnv
4 | from hcraft.render.human import render_env_with_human
5 | from tests.envs import classic_env, player_only_env, zone_only_env
6 |
7 |
8 | def test_render_rgb_array_classic_env():
9 | env = classic_env()[0]
10 | _render_env(env, test_with_human=False)
11 |
12 |
13 | def test_render_rgb_array_player_only_env():
14 | env = player_only_env()[0]
15 | _render_env(env, test_with_human=False)
16 |
17 |
18 | def test_render_rgb_array_zone_only_env():
19 | env = zone_only_env()[0]
20 | _render_env(env, test_with_human=False)
21 |
22 |
23 | def _render_env(env: HcraftEnv, test_with_human: bool = False):
24 | pytest.importorskip("pygame")
25 | pytest.importorskip("pygame_menu")
26 | if test_with_human:
27 | render_env_with_human(env)
28 | env.render(render_mode="rgb_array")
29 | env.close()
30 |
--------------------------------------------------------------------------------
/tests/planning/test_dummy_env.py:
--------------------------------------------------------------------------------
1 | from hcraft.task import PlaceItemTask
2 | from hcraft.purpose import Purpose
3 | from hcraft.elements import Item, Zone, Stack
4 |
5 | from tests.envs import classic_env
6 | import pytest
7 |
8 |
9 | @pytest.mark.slow
10 | def test_hcraft_classic():
11 | pytest.importorskip("unified_planning")
12 | env, _, named_transformations, start_zone, items, zones, zones_items = classic_env()
13 |
14 | task = PlaceItemTask(Stack(Item("table"), 1), Zone("other_zone"))
15 | env.purpose = Purpose(task)
16 |
17 | problem = env.planning_problem(planner_name="aries")
18 | print(problem.upf_problem)
19 |
20 | problem.solve()
21 |
22 | expected_plan = [
23 | "1_search_wood(start)",
24 | "0_move_to_other_zone(start)",
25 | "3_craft_plank(other_zone)",
26 | "4_craft_table(other_zone)",
27 | ]
28 | assert expected_plan == [str(action) for action in problem.plan.actions]
29 |
--------------------------------------------------------------------------------
/src/hcraft/behaviors/actions.py:
--------------------------------------------------------------------------------
1 | """Module to define Action nodes for the HEBGraph of the HierarchyCraft environment."""
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | import numpy as np
6 | from hebg import Action
7 |
8 | from hcraft.render.utils import build_transformation_image
9 |
10 | if TYPE_CHECKING:
11 | from hcraft.env import HcraftEnv
12 | from hcraft.transformation import Transformation
13 |
14 |
15 | class DoTransformation(Action):
16 | """Perform a transformation."""
17 |
18 | def __init__(self, transformation: "Transformation", env: "HcraftEnv") -> None:
19 | image = np.array(
20 | build_transformation_image(transformation, env.world.resources_path)
21 | )
22 | action = env.world.transformations.index(transformation)
23 | self.transformation = transformation
24 | super().__init__(
25 | action,
26 | name=repr(transformation),
27 | image=image,
28 | complexity=1,
29 | )
30 |
--------------------------------------------------------------------------------
/.github/workflows/python-tests-no-optdeps.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python tests (no optional dependencies)
5 |
6 | on: ["push"]
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: windows-latest
12 | strategy:
13 | matrix:
14 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
15 | env:
16 | MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Install uv
20 | uses: astral-sh/setup-uv@v5
21 | - name: Set up venv with Python ${{ matrix.python-version }}
22 | run: |
23 | uv venv --python ${{ matrix.python-version }}
24 | - name: Install dependencies
25 | run: |
26 | uv sync
27 | - name: Test with pytest
28 | run: |
29 | uv run pytest
30 |
--------------------------------------------------------------------------------
/tests/custom_checks.py:
--------------------------------------------------------------------------------
1 | import networkx as nx
2 | import numpy as np
3 | import pytest_check as check
4 | from networkx import is_isomorphic
5 |
6 |
7 | def check_np_equal(array: np.ndarray, expected_array: np.ndarray):
8 | check.is_true(
9 | np.all(array == expected_array),
10 | msg=f"Got:\n{array}\nExpected:\n{expected_array}\nDiff:{array - expected_array}",
11 | )
12 |
13 |
14 | def check_isomorphic(actual_graph: nx.Graph, expected_graph: nx.Graph):
15 | check.is_true(
16 | is_isomorphic(actual_graph, expected_graph),
17 | msg="Graphs are not isomorphic:"
18 | f"\n{list(actual_graph.edges())}"
19 | f"\n{list(expected_graph.edges())}",
20 | )
21 |
22 |
23 | def check_not_isomorphic(actual_graph: nx.Graph, expected_graph: nx.Graph):
24 | check.is_false(
25 | is_isomorphic(actual_graph, expected_graph),
26 | msg="Graphs are isomorphic, yet they shouldn't:"
27 | f"\n{list(actual_graph.edges())}"
28 | f"\n{list(expected_graph.edges())}",
29 | )
30 |
--------------------------------------------------------------------------------
/tests/test_world.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pytest_check as check
3 |
4 | from hcraft.elements import Item, Zone
5 | from hcraft.world import World
6 |
7 |
8 | class TestWorld:
9 | @pytest.fixture(autouse=True)
10 | def setup_method(self):
11 | self.n_items = 5
12 | self.n_zones = 4
13 | self.n_zones_items = 2
14 | self.n_transformations = 6
15 |
16 | self.items = [Item(str(i)) for i in range(self.n_items)]
17 | self.zones = [Zone(str(i)) for i in range(self.n_zones)]
18 | self.zones_items = [Item(f"z{i}") for i in range(self.n_zones_items)]
19 | self.world = World(self.items, self.zones, self.zones_items)
20 |
21 | def test_slot_from_item(self):
22 | item_3 = self.items[3]
23 | check.equal(self.world.slot_from_item(item_3), 3)
24 |
25 | def test_slot_from_zone(self):
26 | zone_3 = self.zones[3]
27 | check.equal(self.world.slot_from_zone(zone_3), 3)
28 |
29 | def test_slot_from_zoneitem(self):
30 | zone_3 = self.zones_items[1]
31 | check.equal(self.world.slot_from_zoneitem(zone_3), 1)
32 |
--------------------------------------------------------------------------------
/.github/workflows/python-coverage.yml:
--------------------------------------------------------------------------------
1 | name: Python coverage
2 |
3 | on: ["push"]
4 |
5 | jobs:
6 | build:
7 | runs-on: windows-latest
8 | env:
9 | MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434
10 | SDL_VIDEODRIVER: "dummy" # for PyGame render https://stackoverflow.com/questions/15933493/pygame-error-no-available-video-device
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Install uv
14 | uses: astral-sh/setup-uv@v5
15 | - name: Install dependencies
16 | run: |
17 | uv sync --extra gym --extra gui --extra planning --extra htmlvis
18 | - name: Setup java for ENHSP
19 | uses: actions/setup-java@v2
20 | with:
21 | distribution: "microsoft"
22 | java-version: "17"
23 | - name: Build coverage using pytest-cov
24 | run: |
25 | uv run pytest --cov=hcraft --cov-report=xml tests
26 | - name: Codacy Coverage Reporter
27 | uses: codacy/codacy-coverage-reporter-action@v1.3.0
28 | with:
29 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
30 | coverage-reports: coverage.xml
31 |
--------------------------------------------------------------------------------
/docs/template/build-search-index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script is invoked by pdoc to precompile the search index.
3 | * Precompiling the search index increases file size, but skips the CPU-heavy index building in the browser.
4 | */
5 | let elasticlunr = require("./resources/elasticlunr.min");
6 |
7 | let fs = require("fs");
8 | let docs = JSON.parse(fs.readFileSync(0, "utf-8"));
9 |
10 | /* mirrored in search.js.jinja2 (part 1) */
11 | elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/);
12 |
13 | /* mirrored in search.js.jinja2 (part 2) */
14 | searchIndex = elasticlunr(function () {
15 | this.pipeline.remove(elasticlunr.stemmer);
16 | this.pipeline.remove(elasticlunr.stopWordFilter);
17 | this.addField("qualname");
18 | this.addField("fullname");
19 | this.addField("annotation");
20 | this.addField("default_value");
21 | this.addField("signature");
22 | this.addField("bases");
23 | this.addField("doc");
24 | this.setRef("fullname");
25 | });
26 | for (let doc of docs) {
27 | searchIndex.addDoc(doc);
28 | }
29 |
30 | process.stdout.write(JSON.stringify(searchIndex.toJSON()));
31 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/tools.py:
--------------------------------------------------------------------------------
1 | """Minecraft Tools
2 |
3 | All used minecraft tools
4 |
5 | """
6 |
7 | from enum import Enum
8 | from typing import Dict, List
9 |
10 | from hcraft.elements import Item
11 |
12 |
13 | class Material(Enum):
14 | """Minecraft materials"""
15 |
16 | WOOD = "wood"
17 | STONE = "stone"
18 | IRON = "iron"
19 | GOLD = "gold"
20 | DIAMOND = "diamond"
21 |
22 |
23 | class ToolType(Enum):
24 | """Minecraft tools types"""
25 |
26 | PICKAXE = "pickaxe"
27 | AXE = "axe"
28 | SHOVEL = "shovel"
29 | SWORD = "sword"
30 |
31 |
32 | MC_TOOLS: List[Item] = []
33 | MC_TOOLS_BY_TYPE_AND_MATERIAL: Dict[ToolType, Dict[Material, Item]] = {}
34 |
35 |
36 | def build_tools():
37 | for tool_type in ToolType:
38 | if tool_type not in MC_TOOLS_BY_TYPE_AND_MATERIAL:
39 | MC_TOOLS_BY_TYPE_AND_MATERIAL[tool_type] = {}
40 | for material in Material:
41 | item = Item(f"{material.value}_{tool_type.value}")
42 | MC_TOOLS_BY_TYPE_AND_MATERIAL[tool_type][material] = item
43 | MC_TOOLS.append(item)
44 |
45 |
46 | build_tools()
47 |
--------------------------------------------------------------------------------
/docs/template/math.html.jinja2:
--------------------------------------------------------------------------------
1 | {# This template is included in math mode and loads MathJax for formula rendering. #}
2 |
13 |
14 |
15 |
24 |
29 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | with import { };
2 |
3 | let
4 | pythonPackages = python3Packages;
5 | in pkgs.mkShell rec {
6 | name = "localDevPythonEnv";
7 | venvDir = "./.venv";
8 | buildInputs = [
9 | # A Python interpreter including the 'venv' module is required to bootstrap
10 | # the environment.
11 | pythonPackages.python
12 |
13 | # This executes some shell code to initialize a venv in $venvDir before
14 | # dropping into the shell
15 | pythonPackages.venvShellHook
16 |
17 | # Those are dependencies that we would like to use from nixpkgs, which will
18 | # add them to PYTHONPATH and thus make them accessible from within the venv.
19 | pythonPackages.numpy
20 | pythonPackages.networkx
21 | pythonPackages.matplotlib
22 | pythonPackages.seaborn
23 | pythonPackages.tqdm
24 | pythonPackages.pygame
25 | ];
26 |
27 | # Run this command, only after creating the virtual environment
28 | postVenvCreation = ''
29 | pip install -e '.[dev,gui,planning,gym]'
30 | '';
31 |
32 | # Now we can execute any commands within the virtual environment.
33 | # This is optional and can be left out to run pip manually.
34 | postShellHook = ''
35 | '';
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to HierarchyCraft
2 |
3 | Whenever you encounter a :bug: **bug** or have :tada: **feature request**,
4 | report this via `Github issues `_.
5 |
6 | We are happy to receive contributions in the form of **pull requests** via Github.
7 | Feel free to fork the repository, implement your changes and create a merge request to the `master` branch.
8 |
9 | ## Build from source (for contributions)
10 |
11 | 1. Clone the repository
12 |
13 | ```bash
14 | git clone https://github.com/IRLL/HierarchyCraft.git
15 | ```
16 |
17 | 2. Install `uv`
18 |
19 | `uv` is a rust-based extremely fast package management tool
20 | that we use for developement.
21 |
22 | See [`uv` installation instructions](https://docs.astral.sh/uv/getting-started/installation/).
23 |
24 | 3. Install all dependencies in a virtual environment using uv
25 |
26 | Install hcraft as an editable package with
27 | dev requirements and all other extra requirements using uv:
28 | ```bash
29 | uv sync --extra gym --extra gui --extra planning --extra htmlvis
30 | ```
31 |
32 | 4. Check installation
33 |
34 | Check installation by running (fast) tests,
35 | remove the marker flag to run even slow tests:
36 | ```bash
37 | pytest -m "not slow"
38 | ```
39 |
--------------------------------------------------------------------------------
/tests/solving_behaviors/test_doc_example.py:
--------------------------------------------------------------------------------
1 | from matplotlib import pyplot as plt
2 | import pytest
3 | import pytest_check as check
4 |
5 |
6 | @pytest.mark.slow
7 | def test_doc_example():
8 | from hcraft.examples import MineHcraftEnv
9 | from hcraft.examples.minecraft.items import DIAMOND
10 | from hcraft.task import GetItemTask
11 |
12 | draw_call_graph = False
13 | render = False
14 |
15 | if draw_call_graph:
16 | _fig, ax = plt.subplots()
17 |
18 | get_diamond = GetItemTask(DIAMOND)
19 | env = MineHcraftEnv(purpose=get_diamond, max_step=50)
20 | solving_behavior = env.solving_behavior(get_diamond)
21 |
22 | done = False
23 | observation, _info = env.reset()
24 | if render:
25 | env.render()
26 | while not done:
27 | action = solving_behavior(observation)
28 |
29 | if draw_call_graph:
30 | plt.cla()
31 | solving_behavior.graph.call_graph.draw(ax)
32 | plt.show(block=False)
33 |
34 | observation, _reward, terminated, truncated, _info = env.step(action)
35 | done = terminated or truncated
36 | if render:
37 | env.render()
38 |
39 | check.is_true(terminated) # Env is successfuly terminated
40 | check.is_true(get_diamond.terminated) # DIAMOND has been obtained !
41 |
--------------------------------------------------------------------------------
/tests/examples/test_random.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pytest_check as check
3 |
4 | from hcraft.examples.random_simple.env import RandomHcraftEnv
5 | from tests.custom_checks import check_isomorphic, check_not_isomorphic
6 |
7 |
8 | class TestRandomHcraft:
9 | """Test the RandomHcraft environment"""
10 |
11 | @pytest.fixture(autouse=True)
12 | def setup_method(self):
13 | """Setup test fixtures."""
14 | self.n_items_per_n_inputs = {0: 1, 1: 5, 2: 10, 4: 1}
15 | self.n_items = sum(self.n_items_per_n_inputs.values())
16 |
17 | def test_same_seed_same_requirements_graph(self):
18 | env = RandomHcraftEnv(self.n_items_per_n_inputs, seed=42)
19 | env2 = RandomHcraftEnv(self.n_items_per_n_inputs, seed=42)
20 | check.equal(env.seed, env2.seed)
21 | check_isomorphic(
22 | env.world.requirements.graph,
23 | env2.world.requirements.graph,
24 | )
25 |
26 | def test_different_seed_different_requirements_graph(self):
27 | env = RandomHcraftEnv(self.n_items_per_n_inputs, seed=42)
28 | env2 = RandomHcraftEnv(self.n_items_per_n_inputs, seed=43)
29 | check.not_equal(env.seed, env2.seed)
30 | check_not_isomorphic(
31 | env.world.requirements.graph,
32 | env2.world.requirements.graph,
33 | )
34 |
--------------------------------------------------------------------------------
/.github/workflows/python-tests-all-optdeps.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python tests (all optional dependencies)
5 |
6 | on: ["push"]
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: windows-latest
12 | strategy:
13 | matrix:
14 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
15 | env:
16 | MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434
17 | SDL_VIDEODRIVER: "dummy" # for PyGame render https://stackoverflow.com/questions/15933493/pygame-error-no-available-video-device
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Install uv
21 | uses: astral-sh/setup-uv@v5
22 | - name: Set up venv with Python ${{ matrix.python-version }}
23 | run: |
24 | uv venv --python ${{ matrix.python-version }}
25 | - name: Install dependencies
26 | run: |
27 | uv sync --extra gym --extra gui --extra planning --extra htmlvis
28 | - name: Setup java for ENHSP
29 | uses: actions/setup-java@v2
30 | with:
31 | distribution: "microsoft"
32 | java-version: "17"
33 | - name: Test with pytest
34 | run: |
35 | uv run pytest
36 |
--------------------------------------------------------------------------------
/tests/solving_behaviors/test_can_solve.py:
--------------------------------------------------------------------------------
1 | from matplotlib import pyplot as plt
2 | import pytest
3 | import pytest_check as check
4 |
5 |
6 | from hcraft.examples import EXAMPLE_ENVS
7 | from hcraft.examples.minecraft import MineHcraftEnv
8 | from hcraft.env import HcraftEnv
9 |
10 |
11 | @pytest.mark.slow
12 | @pytest.mark.parametrize(
13 | "env_class", [env_class for env_class in EXAMPLE_ENVS if env_class != MineHcraftEnv]
14 | )
15 | def test_can_solve(env_class):
16 | env: HcraftEnv = env_class(max_step=50)
17 | draw_call_graph = False
18 |
19 | if draw_call_graph:
20 | _fig, ax = plt.subplots()
21 |
22 | done = False
23 | observation, _infos = env.reset()
24 | for task in env.purpose.best_terminal_group.tasks:
25 | solving_behavior = env.solving_behavior(task)
26 | task_done = task.terminated
27 | while not task_done and not done:
28 | action = solving_behavior(observation)
29 | if draw_call_graph:
30 | plt.cla()
31 | solving_behavior.graph.call_graph.draw(ax)
32 | plt.show(block=False)
33 |
34 | if action == "Impossible":
35 | raise ValueError("Solving behavior could not find a solution.")
36 | observation, _reward, terminated, truncated, _ = env.step(action)
37 | done = terminated or truncated
38 | task_done = task.terminated
39 |
40 | if draw_call_graph:
41 | plt.show()
42 |
43 | check.is_true(env.purpose.terminated)
44 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/empty.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 |
10 | MINICRAFT_NAME = "Empty"
11 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
12 |
13 |
14 | class MiniHCraftEmpty(MiniCraftEnv):
15 | MINICRAFT_NAME = MINICRAFT_NAME
16 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
17 |
18 | ROOM = Zone("room")
19 | """The one and only room."""
20 |
21 | GOAL = Item("goal")
22 | """Goal to reach."""
23 |
24 | def __init__(self, **kwargs) -> None:
25 | self.task = GetItemTask(self.GOAL)
26 | super().__init__(
27 | self.MINICRAFT_NAME,
28 | purpose=self.task,
29 | start_zone=self.ROOM,
30 | **kwargs,
31 | )
32 |
33 | def build_transformations(self) -> List[Transformation]:
34 | find_goal = Transformation(
35 | "find_goal",
36 | inventory_changes=[Yield(CURRENT_ZONE, self.GOAL, max=0)],
37 | zone=self.ROOM,
38 | )
39 |
40 | reach_goal = Transformation(
41 | "reach_goal",
42 | inventory_changes=[
43 | Use(CURRENT_ZONE, self.GOAL, consume=1),
44 | Yield(PLAYER, self.GOAL),
45 | ],
46 | )
47 | return [find_goal, reach_goal]
48 |
--------------------------------------------------------------------------------
/tests/render/test_widgets.py:
--------------------------------------------------------------------------------
1 | import pytest_check as check
2 |
3 | from hcraft.render.widgets import ContentMode, DisplayMode, show_button, show_content
4 |
5 |
6 | class TestShowButton:
7 | def test_all(self):
8 | mode = DisplayMode.ALL
9 | check.is_true(show_button(mode, is_current=False, discovered=False))
10 |
11 | def test_discovered(self):
12 | mode = DisplayMode.DISCOVERED
13 | check.is_false(show_button(mode, is_current=False, discovered=False))
14 | check.is_true(show_button(mode, is_current=False, discovered=True))
15 | check.is_true(show_button(mode, is_current=True, discovered=False))
16 |
17 | def test_current(self):
18 | mode = DisplayMode.CURRENT
19 | check.is_false(show_button(mode, is_current=False, discovered=False))
20 | check.is_false(show_button(mode, is_current=False, discovered=True))
21 | check.is_true(show_button(mode, is_current=True, discovered=False))
22 | check.is_true(show_button(mode, is_current=True, discovered=True))
23 |
24 |
25 | class TestShowTransformationContent:
26 | def test_all(self):
27 | mode = ContentMode.ALWAYS
28 | check.is_true(show_content(mode, discovered=False))
29 |
30 | def test_discovered(self):
31 | mode = ContentMode.DISCOVERED
32 | check.is_false(show_content(mode, discovered=False))
33 | check.is_true(show_content(mode, discovered=True))
34 |
35 | def test_current(self):
36 | mode = ContentMode.NEVER
37 | check.is_false(show_content(mode, discovered=False))
38 | check.is_false(show_content(mode, discovered=True))
39 |
--------------------------------------------------------------------------------
/.github/workflows/pydoc-github-pages.yml:
--------------------------------------------------------------------------------
1 | name: Documentation
2 |
3 | # build the documentation whenever there are new commits on main
4 | on:
5 | push:
6 | branches:
7 | - master
8 | # Alternative: only build for tags.
9 | # tags:
10 | # - '*'
11 |
12 | # security: restrict permissions for CI jobs.
13 | permissions:
14 | contents: read
15 |
16 | jobs:
17 | # Build the documentation and upload the static HTML files as an artifact.
18 | build:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Install uv
23 | uses: astral-sh/setup-uv@v5
24 | - name: Install dependencies
25 | run: |
26 | uv sync --extra docs --extra gym --extra gui --extra planning --extra htmlvis
27 | - name: Make documentation
28 | run: |
29 | uv run pdoc -d google -t docs/template --logo https://irll.net/irll-logo.png --logo-link https://irll.github.io/HierarchyCraft/hcraft.html -o docs/build --math --no-search hcraft
30 | - name: Upload static documentation artifact
31 | uses: actions/upload-pages-artifact@v3
32 | with:
33 | path: docs/build
34 |
35 | # Deploy the artifact to GitHub pages.
36 | # This is a separate job so that only actions/deploy-pages has the necessary permissions.
37 | deploy:
38 | needs: build
39 | runs-on: ubuntu-latest
40 | permissions:
41 | pages: write
42 | id-token: write
43 | environment:
44 | name: github-pages
45 | url: ${{ steps.deployment.outputs.page_url }}
46 | steps:
47 | - id: deployment
48 | uses: actions/deploy-pages@v4
49 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Python development environment with uv";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 | flake-utils.url = "github:numtide/flake-utils";
7 | };
8 |
9 | outputs =
10 | {
11 | self,
12 | nixpkgs,
13 | flake-utils,
14 | }:
15 | flake-utils.lib.eachDefaultSystem (
16 | system:
17 | let
18 | pkgs = import nixpkgs { inherit system; };
19 | python = pkgs.python3;
20 | pythonEnv = python.withPackages (p: [
21 | # Here goes all the libraries that can't be managed by uv because of dynamic linking issues
22 | # or that you just want to be managed by nix for one reason or another
23 | p.numpy
24 | p.networkx
25 | p.matplotlib
26 | p.seaborn
27 | p.tqdm
28 | p.pygame
29 | ]);
30 | in
31 | {
32 | devShells.default =
33 | with pkgs;
34 | mkShell {
35 | packages = [
36 | uv
37 | python
38 | pythonEnv
39 | ];
40 | # this runs when we do `nix develop .`
41 |
42 | shellHook = ''
43 | # Create a virtual environment if it doesn't exist
44 | if [ ! -d ".venv" ]; then
45 | uv venv .venv
46 | fi
47 |
48 | export UV_PYTHON_PREFERENCE="only-system";
49 | export UV_PYTHON=${python}
50 | source .venv/bin/activate
51 |
52 | uv sync --all-extras
53 | echo "Environment ready"
54 | '';
55 | };
56 | }
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1731533236,
9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1756159630,
24 | "narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "NixOS",
32 | "ref": "nixpkgs-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/tests/requirements/test_can_draw.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import TYPE_CHECKING, Type
3 |
4 | import pytest
5 | from pytest_mock import MockerFixture
6 |
7 |
8 | from hcraft.examples import EXAMPLE_ENVS
9 | from hcraft.env import HcraftEnv
10 |
11 | if TYPE_CHECKING:
12 | import matplotlib.pyplot
13 |
14 |
15 | @pytest.mark.slow
16 | @pytest.mark.parametrize("env_class", EXAMPLE_ENVS)
17 | def test_can_draw(env_class: Type[HcraftEnv], mocker: MockerFixture):
18 | draw_plt = True
19 | draw_html = True
20 | save = False
21 | env = env_class()
22 | requirements = env.world.requirements
23 | requirements_dir = Path("docs", "images", "requirements_graphs")
24 |
25 | if draw_plt:
26 | plt: "matplotlib.pyplot" = pytest.importorskip("matplotlib.pyplot")
27 |
28 | width = max(requirements.depth, 10)
29 | height = max(9 / 16 * width, requirements.width / requirements.depth * width)
30 | resolution = max(64 * requirements.depth, 900)
31 | dpi = resolution / width
32 |
33 | fig, ax = plt.subplots()
34 | plt.tight_layout()
35 | fig.set_size_inches(width, height)
36 |
37 | save_path = None
38 | if save:
39 | save_path = requirements_dir / f"{env.name}.png"
40 | requirements.draw(ax, save_path=save_path, dpi=dpi)
41 |
42 | plt.close()
43 |
44 | if draw_html:
45 | pytest.importorskip("pyvis")
46 | mocker.patch("pyvis.network.webbrowser.open")
47 | requirements_dir.mkdir(exist_ok=True)
48 | filepath = requirements_dir / f"{env.name}.html"
49 | if not save:
50 | mocker.patch("pyvis.network.Network.write_html")
51 | requirements.draw(engine="pyvis", save_path=filepath, with_web_uri=True)
52 |
--------------------------------------------------------------------------------
/docs/template/README.md:
--------------------------------------------------------------------------------
1 | # 📑 pdoc templates
2 |
3 | This directory contains pdoc's default templates, which you can extend in your own template directory. See
4 | [the documentation](https://pdoc.dev/docs/pdoc.html#edit-pdocs-html-template) for an example.
5 |
6 | For customization, the most important files are:
7 |
8 | **Main HTML Templates**
9 |
10 | - `default/module.html.jinja2`: Template for documentation pages.
11 | - `default/index.html.jinja2`: Template for the top-level `index.html`.
12 | - `default/frame.html.jinja2`: The common page layout for `module.html.jinja2` and `index.html.jinja2`.
13 |
14 | **CSS Stylesheets**
15 |
16 | - `custom.css`: Empty be default, add custom additional rules here.
17 | - `content.css`: All style definitions for documentation contents.
18 | - `layout.css`: All style definitions for the page layout (navigation, sidebar, ...).
19 | - `theme.css`: Color definitions (see [`examples/dark-mode`](../../examples/dark-mode)).
20 | - `syntax-highlighting.css`: Code snippet style, see below.
21 |
22 | ## Extending templates
23 |
24 | pdoc will first check for `$template.jinja2` before checking `default/$template.jinja2`. This allows you to reuse the
25 | macros from the main templates in `default/`. For example, you can create a `module.html.jinja2` file in your custom
26 | template directory that extends the default template as follows:
27 |
28 | ```html
29 | {% extends "default/module.html.jinja2" %}
30 | {% block title %}new page title{% endblock %}
31 | ```
32 |
33 | ## Syntax Highlighting
34 |
35 | The `syntax-highlighting.css` file contains the CSS styles used for syntax highlighting.
36 | It is generated as follows:
37 |
38 | ```
39 | pygmentize -f html -a .pdoc-code -S > default/syntax-highlighting.css
40 | ```
41 |
42 | The default theme is `default`, with extended padding added to the `.linenos` class.
43 | Alternative color schemes can be tested on [the Pygments website](https://pygments.org/demo/).
44 |
--------------------------------------------------------------------------------
/docs/template/search.js.jinja2:
--------------------------------------------------------------------------------
1 | {#
2 | This file contains the search index and is only loaded on demand (when the user focuses the search field).
3 | We use a JS file instead of JSON as this works with `file://`.
4 | #}
5 | window.pdocSearch = (function(){
6 | {% include "resources/elasticlunr.min.js" %}
7 |
8 | /** pdoc search index */const docs = {{ search_index | safe }};
9 |
10 | // mirrored in build-search-index.js (part 1)
11 | // Also split on html tags. this is a cheap heuristic, but good enough.
12 | elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/);
13 |
14 | let searchIndex;
15 | if (docs._isPrebuiltIndex) {
16 | console.info("using precompiled search index");
17 | searchIndex = elasticlunr.Index.load(docs);
18 | } else {
19 | console.time("building search index");
20 | // mirrored in build-search-index.js (part 2)
21 | searchIndex = elasticlunr(function () {
22 | this.pipeline.remove(elasticlunr.stemmer);
23 | this.pipeline.remove(elasticlunr.stopWordFilter);
24 | this.addField("qualname");
25 | this.addField("fullname");
26 | this.addField("annotation");
27 | this.addField("default_value");
28 | this.addField("signature");
29 | this.addField("bases");
30 | this.addField("doc");
31 | this.setRef("fullname");
32 | });
33 | for (let doc of docs) {
34 | searchIndex.addDoc(doc);
35 | }
36 | console.timeEnd("building search index");
37 | }
38 |
39 | return (term) => searchIndex.search(term, {
40 | fields: {
41 | qualname: {boost: 4},
42 | fullname: {boost: 2},
43 | annotation: {boost: 2},
44 | default_value: {boost: 2},
45 | signature: {boost: 2},
46 | bases: {boost: 2},
47 | doc: {boost: 1},
48 | },
49 | expand: true
50 | });
51 | })();
52 |
--------------------------------------------------------------------------------
/src/hcraft/render/human.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, List, Optional
2 |
3 | if TYPE_CHECKING:
4 | from pygame.event import Event
5 |
6 | from hcraft.env import HcraftEnv
7 |
8 |
9 | def get_human_action(
10 | env: "HcraftEnv",
11 | additional_events: List["Event"] = None,
12 | can_be_none: bool = False,
13 | fps: Optional[float] = None,
14 | ):
15 | """Update the environment rendering and gather potential action given by the UI.
16 |
17 | Args:
18 | env: The running HierarchyCraft environment.
19 | additional_events (Optional): Additional simulated pygame events.
20 | can_be_none: If False, this function will loop on rendering until an action is found.
21 | If True, will return None if no action was found after one rendering update.
22 |
23 | Returns:
24 | The action found using the UI.
25 |
26 | """
27 | action_chosen = False
28 | while not action_chosen:
29 | action = env.render_window.update_rendering(additional_events, fps)
30 | action_chosen = action is not None or can_be_none
31 | return action
32 |
33 |
34 | def render_env_with_human(env: "HcraftEnv", n_episodes: int = 1):
35 | """Render the given environment with human iteractions.
36 |
37 | Args:
38 | env (HcraftEnv): The HierarchyCraft environment to run.
39 | n_episodes (int, optional): Number of episodes to run. Defaults to 1.
40 | """
41 | print("Purpose: ", env.purpose)
42 |
43 | for _ in range(n_episodes):
44 | env.reset()
45 | done = False
46 | total_reward = 0
47 | while not done:
48 | env.render()
49 | action = get_human_action(env)
50 | print(f"Human did: {env.world.transformations[action]}")
51 |
52 | _observation, reward, terminated, truncated, _info = env.step(action)
53 | done = terminated or truncated
54 | total_reward += reward
55 |
56 | print("SCORE: ", total_reward)
57 |
--------------------------------------------------------------------------------
/tests/requirements/test_available_from_start.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pytest_check as check
3 |
4 | from hcraft.elements import Item, Stack, Zone
5 | from hcraft.requirements import _available_in_zones_stacks
6 |
7 |
8 | class TestAvailableFromStart:
9 | @pytest.fixture(autouse=True)
10 | def setup(self):
11 | self.zone = Zone("0")
12 | self.req_item = Item("requirement")
13 | self.req_stack = Stack(self.req_item)
14 |
15 | def test_no_consumed_stacks(self):
16 | check.is_true(
17 | _available_in_zones_stacks(
18 | stacks=None,
19 | zone=self.zone,
20 | zones_stacks={self.zone: []},
21 | )
22 | )
23 |
24 | def test_consumed_stacks_in_start_zone(self):
25 | check.is_true(
26 | _available_in_zones_stacks(
27 | stacks=[self.req_stack],
28 | zone=self.zone,
29 | zones_stacks={self.zone: [self.req_stack]},
30 | )
31 | )
32 |
33 | def test_consumed_stacks_not_in_start_zone(self):
34 | check.is_false(
35 | _available_in_zones_stacks(
36 | stacks=[self.req_stack],
37 | zone=self.zone,
38 | zones_stacks={self.zone: []},
39 | )
40 | )
41 |
42 | def test_consumed_stacks_not_enough_in_start_zone(self):
43 | req_stack = Stack(self.req_item, 2)
44 | check.is_false(
45 | _available_in_zones_stacks(
46 | stacks=[req_stack],
47 | zone=self.zone,
48 | zones_stacks={self.zone: [Stack(self.req_item, 1)]},
49 | )
50 | )
51 |
52 | def test_consumed_stacks_other_in_start_zone(self):
53 | check.is_false(
54 | _available_in_zones_stacks(
55 | stacks=[self.req_stack],
56 | zone=self.zone,
57 | zones_stacks={self.zone: [Stack(Item("1"))]},
58 | )
59 | )
60 |
--------------------------------------------------------------------------------
/tests/planning/test_planning.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Type, List
2 | import warnings
3 | import pytest
4 | from hcraft.env import HcraftEnv
5 | from tests.envs import classic_env
6 |
7 |
8 | class TestPlanning:
9 | @pytest.fixture(autouse=True)
10 | def setup(self, planning_fixture: "PlanningFixture"):
11 | self.fixture = planning_fixture
12 |
13 | def test_warn_no_goal(self):
14 | env, _, _, _, _, _, _ = classic_env()
15 | self.fixture.given_env(env)
16 | self.fixture.when_building_planning_problem()
17 | self.fixture.then_warning_should_be_given(UserWarning, "plans will be empty")
18 |
19 |
20 | @pytest.fixture
21 | def planning_fixture() -> "PlanningFixture":
22 | return PlanningFixture()
23 |
24 |
25 | class PlanningFixture:
26 | def __init__(self) -> None:
27 | pytest.importorskip("unified_planning")
28 |
29 | def given_env(self, env: HcraftEnv) -> None:
30 | self.env = env
31 |
32 | def when_building_planning_problem(self) -> None:
33 | with pytest.warns() as self.warning_records:
34 | self.planning_problem = self.env.planning_problem()
35 |
36 | def then_warning_should_be_given(
37 | self,
38 | warning_type: Type[Warning] = Warning,
39 | match: Optional[str] = None,
40 | ) -> None:
41 | assert _warning_in_records(
42 | warning_records=self.warning_records, warning_type=warning_type, match=match
43 | ), "Could not find required warning"
44 |
45 |
46 | def _warning_in_records(
47 | warning_records: List[warnings.WarningMessage],
48 | warning_type: Type[Warning],
49 | match: Optional[str],
50 | ) -> bool:
51 | for record in warning_records:
52 | if not isinstance(record.message, warning_type):
53 | continue
54 | if match is None:
55 | return True
56 | for arg in record.message.args:
57 | if isinstance(arg, str) and match in arg:
58 | return True
59 | return False
60 |
--------------------------------------------------------------------------------
/tests/planning/test_can_solve.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING, Type
2 |
3 | import pytest
4 | import pytest_check as check
5 |
6 | import os
7 |
8 |
9 | from hcraft.examples import EXAMPLE_ENVS
10 | from hcraft.examples.minecraft import MineHcraftEnv
11 | from hcraft.examples.minicraft import (
12 | MiniHCraftBlockedUnlockPickup,
13 | )
14 | from hcraft.env import HcraftEnv
15 | from hcraft.examples.recursive import RecursiveHcraftEnv
16 |
17 | if TYPE_CHECKING:
18 | from unified_planning.io import PDDLWriter
19 |
20 |
21 | KNOWN_TO_FAIL_FOR_PLANNER = {
22 | "enhsp": [MiniHCraftBlockedUnlockPickup],
23 | "aries": [MiniHCraftBlockedUnlockPickup, RecursiveHcraftEnv],
24 | }
25 |
26 |
27 | @pytest.mark.slow
28 | @pytest.mark.parametrize(
29 | "env_class", [env for env in EXAMPLE_ENVS if env != MineHcraftEnv]
30 | )
31 | @pytest.mark.parametrize("planner_name", ["enhsp", "aries"])
32 | def test_solve_flat(env_class: Type[HcraftEnv], planner_name: str):
33 | up = pytest.importorskip("unified_planning")
34 | write = False
35 | env = env_class(max_step=200)
36 | problem = env.planning_problem(timeout=5, planner_name=planner_name)
37 |
38 | if write:
39 | writer: "PDDLWriter" = up.io.PDDLWriter(problem.upf_problem)
40 | pddl_dir = os.path.join("planning", "pddl", env.name)
41 | os.makedirs(pddl_dir, exist_ok=True)
42 | writer.write_domain(os.path.join(pddl_dir, "domain.pddl"))
43 | writer.write_problem(os.path.join(pddl_dir, "problem.pddl"))
44 |
45 | optional_requirements = {"enhsp": "up_enhsp", "aries": "up_aries"}
46 | pytest.importorskip(optional_requirements[planner_name])
47 |
48 | if env_class in KNOWN_TO_FAIL_FOR_PLANNER[planner_name]:
49 | pytest.xfail(f"{planner_name} planner is known to fail on {env.name}")
50 |
51 | done = False
52 | _observation, _info = env.reset()
53 | while not done:
54 | action = problem.action_from_plan(env.state)
55 | _observation, _reward, terminated, truncated, _info = env.step(action)
56 | done = terminated or truncated
57 | check.is_true(
58 | env.purpose.terminated, msg=f"Plans failed they were :{problem.plans}"
59 | )
60 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/env.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=arguments-differ, inconsistent-return-statements
2 |
3 | """MineHcraft Environment
4 |
5 | HierarchyCraft environment adapted to the Minecraft inventory
6 |
7 | """
8 |
9 | from pathlib import Path
10 |
11 | from hcraft.elements import Stack
12 | from hcraft.env import HcraftEnv
13 | from hcraft.examples.minecraft.items import (
14 | CLOSE_ENDER_PORTAL,
15 | CRAFTABLE_ITEMS,
16 | MC_FINDABLE_ITEMS,
17 | OPEN_NETHER_PORTAL,
18 | PLACABLE_ITEMS,
19 | )
20 | from hcraft.examples.minecraft.tools import MC_TOOLS
21 | from hcraft.examples.minecraft.transformations import (
22 | build_minehcraft_transformations,
23 | )
24 | from hcraft.examples.minecraft.zones import FOREST, MC_ZONES, NETHER, STRONGHOLD
25 | from hcraft.purpose import platinium_purpose
26 | from hcraft.world import world_from_transformations
27 |
28 | ALL_ITEMS = set(
29 | MC_TOOLS + CRAFTABLE_ITEMS + [mcitem.item for mcitem in MC_FINDABLE_ITEMS]
30 | )
31 | """Set of all items"""
32 |
33 |
34 | class MineHcraftEnv(HcraftEnv):
35 | """MineHcraft Environment: A minecraft-like HierarchyCraft Environment.
36 |
37 | Default purpose is None (sandbox).
38 |
39 | """
40 |
41 | def __init__(self, **kwargs):
42 | mc_transformations = build_minehcraft_transformations()
43 | start_zone = kwargs.pop("start_zone", FOREST)
44 | purpose = kwargs.pop("purpose", None)
45 | if purpose == "all":
46 | purpose = get_platinum_purpose()
47 | mc_world = world_from_transformations(
48 | mc_transformations,
49 | start_zone=start_zone,
50 | start_zones_items={
51 | NETHER: [Stack(OPEN_NETHER_PORTAL)],
52 | STRONGHOLD: [Stack(CLOSE_ENDER_PORTAL)],
53 | },
54 | )
55 | mc_world.resources_path = Path(__file__).parent / "resources"
56 | super().__init__(world=mc_world, name="MineHcraft", purpose=purpose, **kwargs)
57 | self.metadata["video.frames_per_second"] = kwargs.pop("fps", 10)
58 |
59 |
60 | def get_platinum_purpose():
61 | return platinium_purpose(
62 | items=list(ALL_ITEMS),
63 | zones=MC_ZONES,
64 | zones_items=PLACABLE_ITEMS,
65 | )
66 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/crossing.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.purpose import Purpose, GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 | MINICRAFT_NAME = "Crossing"
10 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
11 |
12 |
13 | class MiniHCraftCrossing(MiniCraftEnv):
14 | MINICRAFT_NAME = MINICRAFT_NAME
15 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
16 |
17 | ROOM = Zone("room")
18 | """The one and only room."""
19 |
20 | GOAL = Item("goal")
21 | """Goal to reach."""
22 |
23 | LAVA = Item("lava")
24 | """Lava, it burns."""
25 |
26 | def __init__(self, **kwargs) -> None:
27 | purpose = Purpose()
28 | self.task = GetItemTask(self.GOAL)
29 | purpose.add_task(self.task, terminal_groups="goal")
30 | die_in_lava = GetItemTask(self.LAVA, reward=-1)
31 | purpose.add_task(die_in_lava, terminal_groups="die")
32 | super().__init__(
33 | self.MINICRAFT_NAME,
34 | start_zone=self.ROOM,
35 | purpose=purpose,
36 | **kwargs,
37 | )
38 |
39 | def build_transformations(self) -> List[Transformation]:
40 | find_goal = Transformation(
41 | "find_goal",
42 | inventory_changes=[Yield(CURRENT_ZONE, self.GOAL, max=0)],
43 | zone=self.ROOM,
44 | )
45 |
46 | reach_goal = Transformation(
47 | "reach_goal",
48 | inventory_changes=[
49 | Use(CURRENT_ZONE, self.GOAL),
50 | Yield(PLAYER, self.GOAL),
51 | ],
52 | )
53 |
54 | find_lava = Transformation(
55 | "find_lava",
56 | inventory_changes=[Yield(CURRENT_ZONE, self.LAVA, max=0)],
57 | zone=self.ROOM,
58 | )
59 |
60 | reach_lava = Transformation(
61 | "reach_lava",
62 | inventory_changes=[
63 | Use(CURRENT_ZONE, self.LAVA),
64 | Yield(PLAYER, self.LAVA),
65 | ],
66 | )
67 | return [find_goal, reach_goal, find_lava, reach_lava]
68 |
--------------------------------------------------------------------------------
/tests/render/test_render.py:
--------------------------------------------------------------------------------
1 | import pytest_check as check
2 |
3 | from hcraft.render.render import menus_sizes
4 |
5 |
6 | class TestMenusSizes:
7 | def test_no_zone_no_zone_items(self):
8 | shapes = menus_sizes(
9 | n_items=1,
10 | n_zones_items=0,
11 | n_zones=0,
12 | window_shape=[200, 100],
13 | )
14 | check.equal(shapes["actions"], (70, 100))
15 | check.equal(shapes["player"], (130, 100))
16 |
17 | def test_one_zone_no_zone_items(self):
18 | """No need to show position when only one zone"""
19 | shapes = menus_sizes(
20 | n_items=1,
21 | n_zones_items=0,
22 | n_zones=1,
23 | window_shape=[200, 100],
24 | )
25 | check.equal(shapes["zone"], (0, 0))
26 | check.equal(shapes["position"], (0, 0))
27 |
28 | def test_two_zone_no_zone_items(self):
29 | shapes = menus_sizes(
30 | n_items=1,
31 | n_zones_items=0,
32 | n_zones=2,
33 | window_shape=[200, 100],
34 | )
35 | check.equal(shapes["position"], (130, 73))
36 |
37 | def test_one_zone_no_player_item(self):
38 | shapes = menus_sizes(
39 | n_items=0,
40 | n_zones_items=10,
41 | n_zones=1,
42 | window_shape=[200, 100],
43 | )
44 | check.equal(shapes["player"], (0, 0))
45 | check.not_equal(shapes["zone"], (0, 0))
46 |
47 | def test_two_zones_no_player_item(self):
48 | shapes = menus_sizes(
49 | n_items=0,
50 | n_zones_items=10,
51 | n_zones=2,
52 | window_shape=[200, 100],
53 | )
54 | check.equal(shapes["player"], (0, 0))
55 | check.not_equal(shapes["zone"], (0, 0))
56 | check.not_equal(shapes["position"], (0, 0))
57 |
58 | def test_all(self):
59 | shapes = menus_sizes(
60 | n_items=10,
61 | n_zones_items=10,
62 | n_zones=10,
63 | window_shape=[200, 100],
64 | )
65 | check.equal(shapes["actions"], (70, 100))
66 | check.not_equal(shapes["player"], (0, 0))
67 | check.not_equal(shapes["zone"], (0, 0))
68 | check.not_equal(shapes["position"], (0, 0))
69 |
--------------------------------------------------------------------------------
/tests/examples/test_tower.py:
--------------------------------------------------------------------------------
1 | import networkx as nx
2 | import pytest_check as check
3 |
4 | from hcraft.examples.tower import TowerHcraftEnv
5 | from tests.custom_checks import check_isomorphic
6 |
7 |
8 | def test_tower_requirements_graph_1_1():
9 | """should have expected structure
10 |
11 | #1#
12 | #0#
13 |
14 | """
15 | height = 1
16 | width = 1
17 | env = TowerHcraftEnv(height=height, width=width)
18 | expected_graph = nx.DiGraph()
19 | expected_graph.add_edge(0, 1)
20 | check_isomorphic(env.world.requirements.graph, expected_graph)
21 |
22 |
23 | def test_tower_requirements_graph_1_2():
24 | """should have expected structure
25 |
26 | #2#
27 | #01#
28 |
29 | """
30 | height = 1
31 | width = 2
32 | env = TowerHcraftEnv(height=height, width=width)
33 | expected_graph = nx.DiGraph()
34 | expected_graph.add_edge(0, 2)
35 | expected_graph.add_edge(1, 2)
36 |
37 | check_isomorphic(env.world.requirements.graph, expected_graph)
38 |
39 |
40 | def test_tower_requirements_graph_3_2():
41 | """should have expected structure
42 |
43 | #6##
44 | #45#
45 | #23#
46 | #01#
47 |
48 | """
49 | height = 3
50 | width = 2
51 | env = TowerHcraftEnv(height=height, width=width)
52 | expected_graph = nx.DiGraph()
53 | expected_graph.add_edge(5, 6)
54 | expected_graph.add_edge(4, 6)
55 | expected_graph.add_edge(2, 5)
56 | expected_graph.add_edge(3, 5)
57 | expected_graph.add_edge(2, 4)
58 | expected_graph.add_edge(3, 4)
59 | expected_graph.add_edge(0, 3)
60 | expected_graph.add_edge(1, 3)
61 | expected_graph.add_edge(0, 2)
62 | expected_graph.add_edge(1, 2)
63 |
64 | check_isomorphic(env.world.requirements.graph, expected_graph)
65 |
66 |
67 | def test_tower_accessible_items():
68 | """items from the base of the tower should be accessible from the start."""
69 | env = TowerHcraftEnv(height=2, width=3)
70 | searchable_items = env.world.items[: env.width]
71 | for transfo in env.world.transformations:
72 | added_player_items = transfo.get_changes("player", "add")
73 | if any(item in searchable_items for item in added_player_items):
74 | removed_player_items = transfo.get_changes("player", "remove")
75 | check.equal(len(removed_player_items), 0)
76 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/multiroom.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 | MINICRAFT_NAME = "MultiRoom"
10 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
11 |
12 |
13 | class MiniHCraftMultiRoom(MiniCraftEnv):
14 | MINICRAFT_NAME = MINICRAFT_NAME
15 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
16 |
17 | GOAL = Item("goal")
18 | """Goal to reach."""
19 |
20 | def __init__(self, n_rooms: int = 6, **kwargs) -> None:
21 | self.rooms = [Zone(f"Room {i + 1}") for i in range(n_rooms)]
22 | self.task = GetItemTask(self.GOAL)
23 | super().__init__(
24 | self.MINICRAFT_NAME,
25 | purpose=self.task,
26 | start_zone=self.rooms[0],
27 | **kwargs,
28 | )
29 |
30 | def build_transformations(self) -> List[Transformation]:
31 | transformations = []
32 | find_goal = Transformation(
33 | "Find goal",
34 | inventory_changes=[Yield(CURRENT_ZONE, self.GOAL, max=0)],
35 | zone=self.rooms[-1],
36 | )
37 | transformations.append(find_goal)
38 |
39 | reach_goal = Transformation(
40 | "Reach goal",
41 | inventory_changes=[
42 | Use(CURRENT_ZONE, self.GOAL, consume=1),
43 | Yield(PLAYER, self.GOAL),
44 | ],
45 | )
46 | transformations.append(reach_goal)
47 |
48 | for i, room in enumerate(self.rooms):
49 | connected_rooms: List[Zone] = []
50 | if i > 0:
51 | connected_rooms.append(self.rooms[i - 1])
52 | if i < len(self.rooms) - 1:
53 | connected_rooms.append(self.rooms[i + 1])
54 | for connected_room in connected_rooms:
55 | transformations.append(
56 | Transformation(
57 | f"Go to {room.name} from {connected_room.name}",
58 | destination=room,
59 | zone=connected_room,
60 | )
61 | )
62 |
63 | return transformations
64 |
--------------------------------------------------------------------------------
/tests/solving_behaviors/test_mineHcraft.py:
--------------------------------------------------------------------------------
1 | from matplotlib import pyplot as plt
2 | from hcraft.examples.minecraft.env import MineHcraftEnv
3 | from hcraft.task import Task
4 |
5 |
6 | import pytest
7 | import pytest_check as check
8 |
9 | HARD_TASKS = [
10 | "Place open_ender_portal anywhere",
11 | "Go to stronghold",
12 | "Go to end",
13 | "Get ender_dragon_head",
14 | ]
15 |
16 |
17 | @pytest.mark.slow
18 | def test_solving_behaviors():
19 | """All tasks should be solved by their solving behavior."""
20 | draw_call_graph = False
21 |
22 | if draw_call_graph:
23 | _fig, ax = plt.subplots()
24 |
25 | env = MineHcraftEnv(purpose="all", max_step=500)
26 | done = False
27 | observation, _info = env.reset()
28 | tasks_left = env.purpose.tasks.copy()
29 | task = [t for t in tasks_left if t.name == "Place enchanting_table anywhere"][0]
30 | solving_behavior = env.solving_behavior(task)
31 | easy_tasks_left = [task]
32 | while not done and not env.purpose.terminated:
33 | tasks_left = [t for t in tasks_left if not t.terminated]
34 | if task is None:
35 | easy_tasks_left = [t for t in tasks_left if t.name not in HARD_TASKS]
36 | if len(easy_tasks_left) > 0:
37 | task = easy_tasks_left[0]
38 | else:
39 | task = tasks_left[0]
40 | print(f"Task started: {task} (step={env.current_step})")
41 | solving_behavior = env.solving_behavior(task)
42 | action = solving_behavior(observation)
43 | if draw_call_graph:
44 | plt.cla()
45 | solving_behavior.graph.call_graph.draw(ax)
46 | plt.show(block=False)
47 | observation, _reward, terminated, truncated, _info = env.step(action)
48 | done = terminated or truncated
49 | if task.terminated:
50 | print(f"Task finished: {task}, tasks_left: {tasks_left}")
51 | task = None
52 |
53 | if draw_call_graph:
54 | plt.show()
55 | if isinstance(task, Task) and not task.terminated:
56 | print(f"Last unfinished task: {task}")
57 | if set(t.name for t in tasks_left) == set(HARD_TASKS):
58 | pytest.xfail(f"Harder tasks ({HARD_TASKS}) cannot be done for now ...")
59 | check.is_true(env.purpose.terminated, msg=f"tasks not completed: {tasks_left}")
60 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pytest_check as check
3 |
4 | from hcraft.cli import hcraft_cli
5 | from hcraft.examples import (
6 | MineHcraftEnv,
7 | RandomHcraftEnv,
8 | LightRecursiveHcraftEnv,
9 | RecursiveHcraftEnv,
10 | TowerHcraftEnv,
11 | )
12 |
13 | pygame = pytest.importorskip("pygame")
14 |
15 |
16 | ENV_NAMES = ("minecraft", "tower", "recursive", "light-recursive", "random")
17 |
18 |
19 | def test_purposeless_minehcraft_cli():
20 | env = hcraft_cli(["minecraft"])
21 | check.is_instance(env, MineHcraftEnv)
22 |
23 |
24 | def test_purposed_minehcraft_cli():
25 | env = hcraft_cli(
26 | [
27 | "--goal-reward",
28 | "100",
29 | "--get-item",
30 | "diamond",
31 | "--get-item",
32 | "wood",
33 | "minecraft",
34 | ]
35 | )
36 | check.is_instance(env, MineHcraftEnv)
37 |
38 | task_names = set(task.name for task in env.purpose.tasks)
39 | check.equal(task_names, {"Get diamond", "Get wood"})
40 |
41 | for task in env.purpose.tasks:
42 | check.equal(task._reward, 100)
43 |
44 |
45 | @pytest.mark.parametrize("env_name", ENV_NAMES)
46 | def test_maxstep_cli(env_name: str):
47 | env = hcraft_cli(["--max-step", "100", env_name])
48 | check.equal(env.max_step, 100)
49 |
50 |
51 | def test_tower_cli():
52 | env = hcraft_cli(["tower", "--height", "4", "--width", "3"])
53 | check.is_instance(env, TowerHcraftEnv)
54 | check.equal(env.height, 4)
55 | check.equal(env.width, 3)
56 |
57 |
58 | def test_recursive_cli():
59 | env = hcraft_cli(["recursive", "--n-items", "5"])
60 | check.is_instance(env, RecursiveHcraftEnv)
61 | check.equal(env.n_items, 5)
62 |
63 |
64 | def test_light_recursive_cli():
65 | env = hcraft_cli(
66 | ["light-recursive", "--n-items", "5", "--n-required-previous", "2"]
67 | )
68 | check.is_instance(env, LightRecursiveHcraftEnv)
69 | check.equal(env.n_items, 5)
70 |
71 |
72 | def test_random_cli():
73 | env = hcraft_cli(
74 | [
75 | "random",
76 | *("--n-items-0", "2"),
77 | *("--n-items-1", "3"),
78 | *("--n-items-2", "4"),
79 | *("--n-items-3", "5"),
80 | ]
81 | )
82 | check.is_instance(env, RandomHcraftEnv)
83 | check.equal(env.n_items, 14)
84 |
--------------------------------------------------------------------------------
/src/hcraft/metrics.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List, Union
2 |
3 | from hcraft.purpose import Task, TerminalGroup
4 |
5 |
6 | class SuccessCounter:
7 | """Counter of success rates of tasks or terminal groups."""
8 |
9 | def __init__(self, elements: List[Union[Task, TerminalGroup]]) -> None:
10 | self.elements = elements
11 | self.step_states = {}
12 | self.successes: Dict[Union[Task, TerminalGroup], Dict[int, bool]] = {
13 | element: {} for element in self.elements
14 | }
15 |
16 | def step_reset(self):
17 | """Set the state of elements."""
18 | self.step_states = {element: element.terminated for element in self.elements}
19 |
20 | def new_episode(self, episode: int):
21 | """Add a new episode successes."""
22 | for element in self.elements:
23 | self.successes[element][episode] = False
24 | if len(self.successes[element]) > 10:
25 | self.successes[element].pop(episode - 10)
26 |
27 | def update(self, episode: int):
28 | """Update the success state of the given element for the given episode."""
29 | for element in self.elements:
30 | # Just terminated
31 | if element.terminated != self.step_states[element]:
32 | self.successes[element][episode] = True
33 |
34 | @property
35 | def done_infos(self) -> Dict[str, bool]:
36 | return {
37 | self._is_done_str(self._name(element)): element.terminated
38 | for element in self.elements
39 | }
40 |
41 | @property
42 | def rates_infos(self) -> Dict[str, float]:
43 | return {
44 | self._success_str(self._name(element)): self._rate(element)
45 | for element in self.elements
46 | }
47 |
48 | @staticmethod
49 | def _success_str(name: str):
50 | return f"{name} success rate"
51 |
52 | @staticmethod
53 | def _is_done_str(name: str):
54 | return f"{name} is done"
55 |
56 | def _name(self, element: Union[Task, TerminalGroup]):
57 | if isinstance(element, Task):
58 | return element.name
59 | group_name = "Purpose"
60 | if len(self.elements) > 1:
61 | group_name = f"Terminal group '{element.name}'"
62 | return group_name
63 |
64 | def _rate(self, element: Union[Task, TerminalGroup]) -> float:
65 | n_episodes = max(1, len(self.successes[element]))
66 | return sum(self.successes[element].values()) / n_episodes
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env*
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # VScode
132 | .vscode
133 |
134 | # xcf
135 | *.xcf
136 |
137 | .DS_store
138 |
139 | /images
140 | TODO.txt
141 |
142 | # Local planners and planning files
143 | enhsp-dist
144 | *.pddl
145 |
146 | # Paper draft artifacts
147 | paper.pdf
148 | paper.jats
149 | media
150 |
--------------------------------------------------------------------------------
/tests/examples/minecraft/test_requirements_graph.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pytest_check as check
3 |
4 | from hcraft.examples.minecraft.env import MineHcraftEnv
5 | import hcraft.examples.minecraft.items as items
6 | import hcraft.examples.minecraft.zones as zones
7 | from hcraft.examples.minecraft.tools import (
8 | MC_TOOLS_BY_TYPE_AND_MATERIAL,
9 | Material,
10 | ToolType,
11 | )
12 | from hcraft.requirements import RequirementNode, req_node_name
13 |
14 | IRON_PICKAXE = MC_TOOLS_BY_TYPE_AND_MATERIAL[ToolType.PICKAXE][Material.IRON]
15 |
16 |
17 | class TestMineHcraftReqGraph:
18 | @pytest.fixture(autouse=True)
19 | def setup_method(self):
20 | self.env = MineHcraftEnv()
21 | self.graph = self.env.world.requirements.graph
22 |
23 | def test_wood_require_forest(self):
24 | forest_node = req_node_name(zones.FOREST, RequirementNode.ZONE)
25 | check.is_true(self.graph.has_node(forest_node))
26 | wood_node = req_node_name(items.WOOD, RequirementNode.ITEM)
27 | check.is_true(self.graph.has_node(wood_node))
28 | check.is_true(self.graph.has_edge(forest_node, wood_node))
29 |
30 | def test_bedrock_require_iron_pickaxe(self):
31 | bedrock_node = req_node_name(zones.BEDROCK, RequirementNode.ZONE)
32 | check.is_true(self.graph.has_node(bedrock_node))
33 | iron_pickaxe_node = req_node_name(IRON_PICKAXE, RequirementNode.ITEM)
34 | check.is_true(self.graph.has_node(iron_pickaxe_node))
35 | check.is_true(self.graph.has_edge(iron_pickaxe_node, bedrock_node))
36 |
37 | def test_close_portal_require_zone_where_is(self):
38 | stronghold_node = req_node_name(zones.STRONGHOLD, RequirementNode.ZONE)
39 | check.is_true(self.graph.has_node(stronghold_node))
40 | ender_portal_node = req_node_name(
41 | items.CLOSE_ENDER_PORTAL, RequirementNode.ZONE_ITEM
42 | )
43 | check.is_true(self.graph.has_node(ender_portal_node))
44 | check.is_true(self.graph.has_edge(stronghold_node, ender_portal_node))
45 |
46 | def test_close_portal_do_not_require_itself(self):
47 | ender_portal_node = req_node_name(
48 | items.CLOSE_ENDER_PORTAL, RequirementNode.ZONE_ITEM
49 | )
50 | check.is_true(self.graph.has_node(ender_portal_node))
51 | check.is_false(self.graph.has_edge(ender_portal_node, ender_portal_node))
52 |
53 | def test_furnace_do_not_require_coal(self):
54 | furnace_node = req_node_name(items.FURNACE, RequirementNode.ZONE_ITEM)
55 | check.is_true(self.graph.has_node(furnace_node))
56 | coal_node = req_node_name(items.COAL, RequirementNode.ITEM)
57 | check.is_true(self.graph.has_node(coal_node))
58 | check.is_false(self.graph.has_edge(coal_node, furnace_node))
59 |
--------------------------------------------------------------------------------
/src/hcraft/behaviors/utils.py:
--------------------------------------------------------------------------------
1 | """Module for utility functions to apply on handcrafted Behavior."""
2 |
3 | from typing import Dict, Set, Union
4 |
5 | from hebg import Behavior, HEBGraph
6 |
7 | from hcraft.behaviors.behaviors import (
8 | AbleAndPerformTransformation,
9 | GetItem,
10 | ReachZone,
11 | PlaceItem,
12 | )
13 | from hcraft.elements import Item
14 |
15 |
16 | def get_items_in_graph(
17 | graph: HEBGraph,
18 | all_behaviors: Dict[str, Union[GetItem, ReachZone]] = None,
19 | ) -> Set[Item]:
20 | """Get items in a HierarchyCraft HEBGraph.
21 |
22 | Args:
23 | graph (HEBGraph): An of the HierarchyCraft environment.
24 |
25 | Returns:
26 | Set[Item]: Set of items that appears in the given graph.
27 | """
28 | all_behaviors = all_behaviors if all_behaviors is not None else {}
29 | items_in_graph = set()
30 | for node in graph.nodes():
31 | if isinstance(node, Behavior) and node in all_behaviors:
32 | node = all_behaviors[node]
33 | if isinstance(node, GetItem):
34 | items_in_graph.add(node.item)
35 | if isinstance(node, AbleAndPerformTransformation):
36 | items_in_graph |= node.transformation.production("player")
37 | items_in_graph |= node.transformation.consumption("player")
38 | items_in_graph |= node.transformation.min_required("player")
39 | items_in_graph |= node.transformation.max_required("player")
40 | return items_in_graph
41 |
42 |
43 | def get_zones_items_in_graph(
44 | graph: HEBGraph,
45 | all_behaviors: Dict[str, Union[GetItem, ReachZone]] = None,
46 | ) -> Set[Item]:
47 | """Get properties in a HierarchyCraft HEBGraph.
48 |
49 | Args:
50 | graph (HEBGraph): An HEBehavior graph of the HierarchyCraft environment.
51 | all_behaviors (Dict[str, Union[GetItem, ReachZone]): References to all known behaviors.
52 |
53 | Returns:
54 | Set[Item]: Set of zone items that appears in the given graph.
55 | """
56 | all_behaviors = all_behaviors if all_behaviors is not None else {}
57 | zone_items_in_graph = set()
58 | for node in graph.nodes():
59 | if isinstance(node, Behavior) and node in all_behaviors:
60 | node = all_behaviors[node]
61 | if isinstance(node, PlaceItem):
62 | zone_items_in_graph.add(node.item)
63 | if isinstance(node, AbleAndPerformTransformation):
64 | zone_items_in_graph |= node.transformation.produced_zones_items
65 | zone_items_in_graph |= node.transformation.consumed_zones_items
66 | zone_items_in_graph |= node.transformation.min_required_zones_items
67 | zone_items_in_graph |= node.transformation.max_required_zones_items
68 | return zone_items_in_graph
69 |
--------------------------------------------------------------------------------
/src/hcraft/examples/recursive.py:
--------------------------------------------------------------------------------
1 | """# Recursive HierarchyCraft Environments
2 |
3 | The goal of the environment is to get the last item.
4 | But each item requires all the previous items,
5 | hence the number of actions required is exponential with the number of items.
6 |
7 | Example:
8 | >>> env = RecursiveHcraft(n_items=4)
9 | For example, if there is 4 items, the last item is 3.
10 | But 3 requires all previous items: {2, 1, 0}.
11 | And 2 requires all previous items: {1, 0}.
12 | And 1 requires all previous items: {0}.
13 | Finally Item 0 can be obtained directly.
14 | Thus the number of actions required is $1 + 2 + 4 + 1 = 8 = 2^4$.
15 |
16 | Requirements graph (n_items=6):
17 |
18 | .. include:: ../../../docs/images/requirements_graphs/RecursiveHcraft-I6.html
19 |
20 | """
21 |
22 | from typing import List
23 |
24 | from hcraft.elements import Item
25 | from hcraft.env import HcraftEnv
26 | from hcraft.transformation import Transformation, Use, Yield, PLAYER
27 | from hcraft.task import GetItemTask
28 | from hcraft.world import world_from_transformations
29 |
30 | # gym is an optional dependency
31 | try:
32 | import gymnasium as gym
33 |
34 | gym.register(
35 | id="RecursiveHcraft-v1",
36 | entry_point="hcraft.examples.recursive:RecursiveHcraftEnv",
37 | )
38 |
39 | except ImportError:
40 | pass
41 |
42 |
43 | class RecursiveHcraftEnv(HcraftEnv):
44 | """RecursiveHcraft Environment"""
45 |
46 | def __init__(self, n_items: int = 6, **kwargs):
47 | items = [Item(str(i)) for i in range(n_items)]
48 | self.n_items = n_items
49 | transformations = self.build_transformations(items)
50 | world = world_from_transformations(transformations)
51 | if "purpose" not in kwargs:
52 | kwargs["purpose"] = GetItemTask(items[-1])
53 | super().__init__(
54 | world,
55 | name=f"RecursiveHcraft-I{n_items}",
56 | **kwargs,
57 | )
58 |
59 | def build_transformations(self, items: List[Item]) -> List[Transformation]:
60 | """Build transformations to make every item accessible.
61 |
62 | Args:
63 | items: List of items.
64 |
65 | Returns:
66 | List of transformations.
67 |
68 | """
69 | transformation = []
70 |
71 | for index, item in enumerate(items):
72 | inventory_changes = [Yield(PLAYER, item)]
73 | if index > 0:
74 | inventory_changes += [
75 | Use(PLAYER, items[item_id], consume=1) for item_id in range(index)
76 | ]
77 | new_recipe = Transformation(inventory_changes=inventory_changes)
78 | transformation.append(new_recipe)
79 |
80 | return transformation
81 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/fourrooms.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, List, TypeVar
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 |
10 | MINICRAFT_NAME = "FourRooms"
11 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
12 |
13 |
14 | class MiniHCraftFourRooms(MiniCraftEnv):
15 | MINICRAFT_NAME = MINICRAFT_NAME
16 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
17 |
18 | SOUTH_WEST_ROOM = Zone("South-West")
19 | """South west room."""
20 |
21 | SOUTH_EAST_ROOM = Zone("South-East")
22 | """South east room."""
23 |
24 | NORTH_WEST_ROOM = Zone("North-West")
25 | """North west room."""
26 |
27 | NORTH_EAST_ROOM = Zone("North-East")
28 | """North east room."""
29 |
30 | GOAL = Item("goal")
31 | """Goal to reach."""
32 |
33 | def __init__(self, **kwargs) -> None:
34 | self.task = GetItemTask(self.GOAL)
35 |
36 | self.ROOMS = [
37 | self.SOUTH_WEST_ROOM,
38 | self.SOUTH_EAST_ROOM,
39 | self.NORTH_EAST_ROOM,
40 | self.NORTH_WEST_ROOM,
41 | ]
42 |
43 | super().__init__(
44 | self.MINICRAFT_NAME,
45 | start_zone=self.SOUTH_WEST_ROOM,
46 | purpose=self.task,
47 | **kwargs,
48 | )
49 |
50 | def build_transformations(self) -> List[Transformation]:
51 | find_goal = Transformation(
52 | "find-goal",
53 | inventory_changes=[Yield(CURRENT_ZONE, self.GOAL, max=0)],
54 | zone=self.NORTH_EAST_ROOM,
55 | )
56 |
57 | reach_goal = Transformation(
58 | "reach-goal",
59 | inventory_changes=[
60 | Use(CURRENT_ZONE, self.GOAL, consume=1),
61 | Yield(PLAYER, self.GOAL),
62 | ],
63 | )
64 |
65 | neighbors = _get_rooms_connections(self.ROOMS)
66 |
67 | moves = []
68 | for destination, neighbor_rooms in neighbors.items():
69 | for neighbor_room in neighbor_rooms:
70 | moves.append(
71 | Transformation(
72 | f"go-to-{destination.name}-from-{neighbor_room.name}",
73 | destination=destination,
74 | zone=neighbor_room,
75 | )
76 | )
77 |
78 | return [find_goal, reach_goal] + moves
79 |
80 |
81 | Room = TypeVar("Room")
82 |
83 |
84 | def _get_rooms_connections(rooms: List[Room]) -> Dict[Room, List[Room]]:
85 | neighbors = {}
86 | for room_id, destination in enumerate(rooms):
87 | neighbors[destination] = [rooms[room_id - 1], rooms[(room_id + 1) % len(rooms)]]
88 | return neighbors
89 |
--------------------------------------------------------------------------------
/tests/envs.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.env import HcraftEnv
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 | from hcraft.world import world_from_transformations
7 |
8 |
9 | def classic_env(player=PLAYER, current_zone=CURRENT_ZONE, include_move=True):
10 | start_zone = Zone("start")
11 | other_zone = Zone("other_zone")
12 | zones = [start_zone, other_zone]
13 |
14 | transformations: List[Transformation] = []
15 | if include_move:
16 | move_to_other_zone = Transformation(
17 | "move_to_other_zone",
18 | destination=other_zone,
19 | zone=start_zone,
20 | )
21 | transformations.append(move_to_other_zone)
22 |
23 | wood = Item("wood")
24 |
25 | search_wood_restriction = {"zone": start_zone} if include_move else {}
26 | search_wood = Transformation(
27 | "search_wood",
28 | inventory_changes=[Yield(player, wood)],
29 | **search_wood_restriction,
30 | )
31 | transformations.append(search_wood)
32 |
33 | stone = Item("stone")
34 | search_stone = Transformation(
35 | "search_stone",
36 | inventory_changes=[Yield(player, stone)],
37 | )
38 | transformations.append(search_stone)
39 |
40 | plank = Item("plank")
41 | craft_plank = Transformation(
42 | "craft_plank",
43 | inventory_changes=[
44 | Use(player, wood, consume=1),
45 | Yield(player, plank, create=4),
46 | ],
47 | )
48 | transformations.append(craft_plank)
49 |
50 | table = Item("table")
51 | craft_table = Transformation(
52 | "craft_table",
53 | inventory_changes=[
54 | Use(player, plank, consume=4),
55 | Yield(current_zone, table),
56 | ],
57 | )
58 | transformations.append(craft_table)
59 |
60 | wood_house = Item("wood house")
61 | build_house = Transformation(
62 | "build_house",
63 | inventory_changes=[
64 | Use(player, plank, consume=32),
65 | Use(player, wood, consume=8),
66 | Yield(current_zone, wood_house),
67 | ],
68 | )
69 | transformations.append(build_house)
70 |
71 | items = [wood, stone, plank]
72 | zones_items = [table, wood_house]
73 | named_transformations = {t.name: t for t in transformations}
74 |
75 | start_zone = start_zone if include_move else None
76 | world = world_from_transformations(
77 | transformations=list(named_transformations.values()),
78 | start_zone=start_zone,
79 | )
80 | env = HcraftEnv(world)
81 | return env, world, named_transformations, start_zone, items, zones, zones_items
82 |
83 |
84 | def player_only_env():
85 | return classic_env(current_zone=PLAYER, include_move=False)
86 |
87 |
88 | def zone_only_env():
89 | return classic_env(player=CURRENT_ZONE)
90 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minecraft/__init__.py:
--------------------------------------------------------------------------------
1 | """# MineHcraft: Inspired from the popular game Minecraft.
2 |
3 |
4 |
5 | A rather large and complex requirements graph:
6 |
7 | .. include:: ../../../../docs/images/requirements_graphs/MineHcraft.html
8 |
9 | """
10 |
11 | from typing import Optional
12 |
13 | import hcraft.examples.minecraft.items as items
14 | from hcraft.examples.minecraft.env import ALL_ITEMS, MineHcraftEnv
15 |
16 | from hcraft.purpose import Purpose, RewardShaping
17 | from hcraft.task import GetItemTask
18 |
19 | MINEHCRAFT_GYM_ENVS = []
20 | __all__ = ["MineHcraftEnv"]
21 |
22 |
23 | # gym is an optional dependency
24 | try:
25 | import gymnasium as gym
26 |
27 | ENV_PATH = "hcraft.examples.minecraft.env:MineHcraftEnv"
28 |
29 | # Simple MineHcraft with no reward, only penalty on illegal actions
30 | gym.register(
31 | id="MineHcraft-NoReward-v1",
32 | entry_point=ENV_PATH,
33 | kwargs={"purpose": None},
34 | )
35 | MINEHCRAFT_GYM_ENVS.append("MineHcraft-NoReward-v1")
36 |
37 | # Get all items, place all zones_items and go everywhere
38 | gym.register(
39 | id="MineHcraft-v1",
40 | entry_point=ENV_PATH,
41 | kwargs={"purpose": "all"},
42 | )
43 | MINEHCRAFT_GYM_ENVS.append("MineHcraft-v1")
44 |
45 | def _to_camel_case(name: str):
46 | return "".join([subname.capitalize() for subname in name.split("_")])
47 |
48 | def _register_minehcraft_single_item(
49 | item: items.Item,
50 | name: Optional[str] = None,
51 | success_reward: float = 10.0,
52 | timestep_reward: float = -0.1,
53 | reward_shaping: RewardShaping = RewardShaping.REQUIREMENTS_ACHIVEMENTS,
54 | version: int = 1,
55 | ):
56 | purpose = Purpose(timestep_reward=timestep_reward)
57 | purpose.add_task(
58 | GetItemTask(item, reward=success_reward),
59 | reward_shaping=reward_shaping,
60 | )
61 | if name is None:
62 | name = _to_camel_case(item.name)
63 | gym_name = f"MineHcraft-{name}-v{version}"
64 | gym.register(
65 | id=gym_name,
66 | entry_point=ENV_PATH,
67 | kwargs={"purpose": purpose},
68 | )
69 | MINEHCRAFT_GYM_ENVS.append(gym_name)
70 |
71 | replacement_names = {
72 | items.COBBLESTONE: "Stone",
73 | items.IRON_INGOT: "Iron",
74 | items.GOLD_INGOT: "Gold",
75 | items.ENDER_DRAGON_HEAD: "Dragon",
76 | }
77 |
78 | for item in ALL_ITEMS:
79 | cap_item_name = "".join([part.capitalize() for part in item.name.split("_")])
80 | item_id = replacement_names.get(item, cap_item_name)
81 | _register_minehcraft_single_item(item, name=item_id)
82 |
83 |
84 | except ImportError:
85 | pass
86 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/unlock.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import PlaceItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 |
10 | MINICRAFT_NAME = "Unlock"
11 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
12 |
13 |
14 | class MiniHCraftUnlock(MiniCraftEnv):
15 | MINICRAFT_NAME = MINICRAFT_NAME
16 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
17 |
18 | START = Zone("start_room")
19 | """Start room."""
20 |
21 | KEY = Item("key")
22 | """Key used to unlock the door."""
23 |
24 | OPEN_DOOR = Item("open_door")
25 | """Open door between the two rooms."""
26 | LOCKED_DOOR = Item("locked_door")
27 | """Locked door between the two rooms, can be unlocked with a key."""
28 |
29 | def __init__(self, **kwargs) -> None:
30 | self.task = PlaceItemTask(self.OPEN_DOOR)
31 | super().__init__(
32 | self.MINICRAFT_NAME,
33 | purpose=self.task,
34 | start_zone=self.START,
35 | **kwargs,
36 | )
37 |
38 | def build_transformations(self) -> List[Transformation]:
39 | transformations = []
40 |
41 | search_for_key = Transformation(
42 | "search_for_key",
43 | inventory_changes=[
44 | Yield(CURRENT_ZONE, self.KEY, create=1, max=0),
45 | Yield(PLAYER, self.KEY, create=0, max=0),
46 | ],
47 | zone=self.START,
48 | )
49 | transformations.append(search_for_key)
50 |
51 | pickup = Transformation(
52 | "pickup_key",
53 | inventory_changes=[
54 | Use(CURRENT_ZONE, self.KEY, consume=1),
55 | Yield(PLAYER, self.KEY, create=1),
56 | ],
57 | )
58 | put_down = Transformation(
59 | "put_down_key",
60 | inventory_changes=[
61 | Use(PLAYER, self.KEY, consume=1),
62 | Yield(CURRENT_ZONE, self.KEY, create=1),
63 | ],
64 | )
65 | transformations += [pickup, put_down]
66 |
67 | search_for_door = Transformation(
68 | "search_for_door",
69 | inventory_changes=[
70 | Yield(CURRENT_ZONE, self.LOCKED_DOOR, max=0),
71 | Yield(CURRENT_ZONE, self.OPEN_DOOR, create=0, max=0),
72 | ],
73 | zone=self.START,
74 | )
75 | transformations.append(search_for_door)
76 |
77 | unlock_door = Transformation(
78 | "unlock_door",
79 | inventory_changes=[
80 | Use(PLAYER, self.KEY),
81 | Use(CURRENT_ZONE, self.LOCKED_DOOR, consume=1),
82 | Yield(CURRENT_ZONE, self.OPEN_DOOR, create=1),
83 | ],
84 | )
85 | transformations.append(unlock_door)
86 |
87 | return transformations
88 |
--------------------------------------------------------------------------------
/src/hcraft/examples/light_recursive.py:
--------------------------------------------------------------------------------
1 | """LightRecursive, a less recursive version of the RecursiveHcraft Environment.
2 |
3 | Item n requires one of each the k previous items (n-k to n-1).
4 |
5 | Example:
6 | >>> env = LightRecursiveHcraftEnv(n_items=4, n_required_previous=2)
7 | For example, if there is 5 items, the last item is 4.
8 | But 4 requires the 2 previous items: {3, 2}.
9 | And 3 requires the 2 previous items: {2, 1}.
10 | And 2 requires the 2 previous items: {1, 0}.
11 | And 1 requires the only previous items: {0}.
12 | Finally Item 0 can be obtained directly.
13 |
14 | Requirements graph (n_items=6, n_required_previous=2):
15 |
16 | .. include:: ../../../docs/images/requirements_graphs/LightRecursiveHcraft-K2-I6.html
17 |
18 | """
19 |
20 | from hcraft.elements import Item
21 | from hcraft.env import HcraftEnv
22 | from hcraft.task import GetItemTask
23 | from hcraft.transformation import PLAYER, Transformation, Use, Yield
24 | from hcraft.world import world_from_transformations
25 |
26 |
27 | from typing import List
28 |
29 |
30 | # gym is an optional dependency
31 | try:
32 | import gymnasium as gym
33 |
34 | gym.register(
35 | id="LightRecursiveHcraft-v1",
36 | entry_point="hcraft.examples.light_recursive:LightRecursiveHcraftEnv",
37 | )
38 |
39 | except ImportError:
40 | pass
41 |
42 |
43 | class LightRecursiveHcraftEnv(HcraftEnv):
44 | """LightRecursive environment."""
45 |
46 | def __init__(self, n_items: int = 6, n_required_previous: int = 2, **kwargs):
47 | self.n_items = n_items
48 | self.n_required_previous = n_required_previous
49 | if n_required_previous == 1:
50 | env_name = "LinearRecursiveHcraft"
51 | else:
52 | env_name = f"LightRecursiveHcraft-K{n_required_previous}-I{n_items}"
53 | items = [Item(str(i)) for i in range(n_items)]
54 | transformations = self._transformations(items)
55 | world = world_from_transformations(transformations)
56 | if "purpose" not in kwargs:
57 | kwargs["purpose"] = GetItemTask(items[-1])
58 | super().__init__(world, name=env_name, **kwargs)
59 |
60 | def _transformations(self, items: List[Item]) -> List[Transformation]:
61 | """Build recipes to make every item accessible.
62 |
63 | Args:
64 | items: List of items.
65 |
66 | Returns:
67 | List of craft recipes.
68 |
69 | """
70 | transformation = []
71 |
72 | for index, item in enumerate(items):
73 | low_id = max(0, index - self.n_required_previous)
74 | inventory_changes = [Yield(PLAYER, item)]
75 | if index > 0:
76 | inventory_changes += [
77 | Use(PLAYER, items[item_id], consume=1)
78 | for item_id in range(low_id, index)
79 | ]
80 |
81 | new_recipe = Transformation(inventory_changes=inventory_changes)
82 | transformation.append(new_recipe)
83 |
84 | return transformation
85 |
--------------------------------------------------------------------------------
/tests/examples/test_recursive.py:
--------------------------------------------------------------------------------
1 | import networkx as nx
2 | import pytest_check as check
3 | from hcraft.examples.light_recursive import LightRecursiveHcraftEnv
4 |
5 | from hcraft.examples.recursive import RecursiveHcraftEnv
6 | from tests.custom_checks import check_isomorphic
7 |
8 |
9 | def test_recursive_requirements_graph():
10 | n_items = 4
11 | expected_graph = nx.DiGraph()
12 | expected_graph.add_edge(0, 1)
13 | expected_graph.add_edge(0, 2)
14 | expected_graph.add_edge(0, 3)
15 | expected_graph.add_edge(1, 2)
16 | expected_graph.add_edge(1, 3)
17 | expected_graph.add_edge(2, 3)
18 |
19 | env = RecursiveHcraftEnv(n_items=n_items)
20 | check_isomorphic(env.world.requirements.graph, expected_graph)
21 |
22 |
23 | def test_solve_recursive():
24 | n_items = 4 # [0, 1, 2, 3]
25 | actions = [
26 | 0, # 0
27 | 1, # 1 < 0
28 | 0, # 0
29 | 2, # 2 < 0 + 1
30 | 0, # 0
31 | 1, # 1
32 | 0, # 0
33 | 3, # 3 < 0 + 1 + 2
34 | ]
35 |
36 | env = RecursiveHcraftEnv(n_items=n_items)
37 | env.reset()
38 | for action in actions:
39 | observation, _reward, terminated, _truncated, _info = env.step(action)
40 | # Should only see items because no zones
41 | check.equal(observation.shape, (n_items,))
42 |
43 | check.is_true(terminated)
44 |
45 |
46 | def test_light_recursive_requirements_graph():
47 | n_items = 6
48 | n_required_previous = 3
49 | env = LightRecursiveHcraftEnv(
50 | n_items=n_items,
51 | n_required_previous=n_required_previous,
52 | )
53 | expected_graph = nx.DiGraph()
54 | expected_graph.add_edge(0, 1)
55 |
56 | expected_graph.add_edge(0, 2)
57 | expected_graph.add_edge(1, 2)
58 |
59 | expected_graph.add_edge(0, 3)
60 | expected_graph.add_edge(1, 3)
61 | expected_graph.add_edge(2, 3)
62 |
63 | expected_graph.add_edge(1, 4)
64 | expected_graph.add_edge(2, 4)
65 | expected_graph.add_edge(3, 4)
66 |
67 | expected_graph.add_edge(2, 5)
68 | expected_graph.add_edge(3, 5)
69 | expected_graph.add_edge(4, 5)
70 |
71 | check_isomorphic(env.world.requirements.graph, expected_graph)
72 |
73 |
74 | def test_solve_light_recursive():
75 | n_items = 5 # [0, 1, 2, 3, 4]
76 | n_required_previous = 2
77 | actions = [
78 | 0, # 0
79 | 1, # 1 < 0
80 | 0, # 0
81 | 2, # 2 < 0 + 1
82 | 0, # 0
83 | 1, # 1
84 | 3, # 3 < 1 + 2
85 | 0, # 0
86 | 1, # 1
87 | 0, # 0
88 | 2, # 2 < 0 + 1
89 | 4, # 4 < 2 + 3
90 | ]
91 |
92 | env = LightRecursiveHcraftEnv(
93 | n_items=n_items,
94 | n_required_previous=n_required_previous,
95 | )
96 | env.reset()
97 | for action in actions:
98 | observation, _reward, terminated, _truncated, _info = env.step(action)
99 | # Should only see items because no zones
100 | check.equal(observation.shape, (n_items,))
101 |
102 | check.is_true(terminated)
103 |
--------------------------------------------------------------------------------
/src/hcraft/examples/treasure/env.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import List
3 |
4 | from hcraft.elements import Item, Zone
5 | from hcraft.env import HcraftEnv
6 | from hcraft.purpose import GetItemTask
7 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
8 | from hcraft.world import world_from_transformations
9 |
10 |
11 | class TreasureEnv(HcraftEnv):
12 | """A simple environment used in for the env building tutorial."""
13 |
14 | TREASURE_ROOM = Zone("treasure_room")
15 | """Room containing the treasure."""
16 | KEY_ROOM = Zone("key_room")
17 | """Where all the keys are stored."""
18 | START_ROOM = Zone("start_room")
19 | """Where the player starts."""
20 |
21 | CHEST = Item("treasure_chest")
22 | """Treasure chest containing gold."""
23 | LOCKED_CHEST = Item("locked_chest")
24 | """Treasure chest containing gold ... but it's locked."""
25 | GOLD = Item("gold")
26 | """Gold! well the pixel version at least."""
27 | KEY = Item("key")
28 | """A key ... it can probably unlock things."""
29 |
30 | def __init__(self, **kwargs) -> None:
31 | transformations = self._build_transformations()
32 | world = world_from_transformations(
33 | transformations=transformations,
34 | start_zone=self.START_ROOM,
35 | start_zones_items={self.TREASURE_ROOM: [self.LOCKED_CHEST]},
36 | )
37 | world.resources_path = Path(__file__).parent / "resources"
38 | super().__init__(
39 | world, purpose=GetItemTask(self.GOLD), name="TreasureHcraft", **kwargs
40 | )
41 |
42 | def _build_transformations(self) -> List[Transformation]:
43 | TAKE_GOLD_FROM_CHEST = Transformation(
44 | "take-gold-from-chest",
45 | inventory_changes=[
46 | Use(CURRENT_ZONE, self.CHEST, consume=1),
47 | Yield(PLAYER, self.GOLD),
48 | ],
49 | )
50 |
51 | SEARCH_KEY = Transformation(
52 | "search-key",
53 | inventory_changes=[
54 | Yield(PLAYER, self.KEY, max=1),
55 | ],
56 | zone=self.KEY_ROOM,
57 | )
58 |
59 | UNLOCK_CHEST = Transformation(
60 | "unlock-chest",
61 | inventory_changes=[
62 | Use(PLAYER, self.KEY, 2),
63 | Use(CURRENT_ZONE, self.LOCKED_CHEST, consume=1),
64 | Yield(CURRENT_ZONE, self.CHEST),
65 | ],
66 | )
67 |
68 | MOVE_TO_KEY_ROOM = Transformation(
69 | "move-to-key_room",
70 | destination=self.KEY_ROOM,
71 | zone=self.START_ROOM,
72 | )
73 | MOVE_TO_TREASURE_ROOM = Transformation(
74 | "move-to-treasure_room",
75 | destination=self.TREASURE_ROOM,
76 | zone=self.START_ROOM,
77 | )
78 | MOVE_TO_START_ROOM = Transformation(
79 | "move-to-start_room",
80 | destination=self.START_ROOM,
81 | )
82 |
83 | return [
84 | TAKE_GOLD_FROM_CHEST,
85 | SEARCH_KEY,
86 | UNLOCK_CHEST,
87 | MOVE_TO_KEY_ROOM,
88 | MOVE_TO_TREASURE_ROOM,
89 | MOVE_TO_START_ROOM,
90 | ]
91 |
--------------------------------------------------------------------------------
/src/hcraft/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | .. include:: ../../README.md
3 |
4 | ## Custom purposes for agents in HierarchyCraft environments
5 |
6 | HierarchyCraft allows users to specify custom purposes (one or multiple tasks) for agents in their environments.
7 | This feature provides a high degree of flexibility and allows users to design environments that
8 | are tailored to specific applications or scenarios.
9 | This feature enables to study mutli-task or lifelong learning settings.
10 |
11 | See [`hcraft.purpose`](https://irll.github.io/HierarchyCraft/hcraft/purpose.html) for more details.
12 |
13 | ## Solving behavior for all tasks of most HierarchyCraft environments
14 |
15 | HierarchyCraft also includes solving behaviors that can be used to generate actions
16 | from observations that will complete most tasks in any HierarchyCraft environment, including user-designed.
17 | Solving behaviors are handcrafted, and may not work in some edge cases when some items are rquired in specific zones.
18 | This feature makes it easy for users to obtain a strong baseline in their custom environments.
19 |
20 | See [`hcraft.solving_behaviors`](https://irll.github.io/HierarchyCraft/hcraft/solving_behaviors.html) for more details.
21 |
22 | ## Visualizing the underlying hierarchy of the environment (requirements graph)
23 |
24 | HierarchyCraft gives the ability to visualize the hierarchy of the environment as a requirements graph.
25 | This graph provides a potentialy complex but complete representation of what is required
26 | to obtain each item or to go in each zone, allowing users to easily understand the structure
27 | of the environment and identify key items of the environment.
28 |
29 | For example, here is the graph of the 'MiniCraftUnlock' environment where the goal is to open a door using a key:
30 | 
31 |
32 |
33 | And here is much more complex graph of the 'MineHcraft' environment shown previously:
34 | 
35 |
36 | See [`hcraft.requirements`](https://irll.github.io/HierarchyCraft/hcraft/requirements.html) for more details.
37 |
38 | """
39 |
40 | import hcraft.state as state
41 | import hcraft.solving_behaviors as solving_behaviors
42 | import hcraft.purpose as purpose
43 | import hcraft.transformation as transformation
44 | import hcraft.requirements as requirements
45 | import hcraft.env as env
46 | import hcraft.examples as examples
47 | import hcraft.world as world
48 | import hcraft.planning as planning
49 |
50 | from hcraft.elements import Item, Stack, Zone
51 | from hcraft.transformation import Transformation
52 | from hcraft.env import HcraftEnv, HcraftState
53 | from hcraft.purpose import Purpose
54 | from hcraft.render.human import get_human_action, render_env_with_human
55 | from hcraft.task import GetItemTask, GoToZoneTask, PlaceItemTask
56 |
57 |
58 | __all__ = [
59 | "HcraftState",
60 | "Transformation",
61 | "Item",
62 | "Stack",
63 | "Zone",
64 | "HcraftEnv",
65 | "get_human_action",
66 | "render_env_with_human",
67 | "Purpose",
68 | "GetItemTask",
69 | "GoToZoneTask",
70 | "PlaceItemTask",
71 | "state",
72 | "transformation",
73 | "purpose",
74 | "solving_behaviors",
75 | "requirements",
76 | "world",
77 | "env",
78 | "planning",
79 | "examples",
80 | ]
81 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/minicraft.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from typing import Optional, List, Dict, Union
4 | from abc import abstractmethod
5 |
6 | from hcraft.elements import Item, Stack, Zone
7 | from hcraft.transformation import Transformation
8 | from hcraft.env import HcraftEnv
9 |
10 | from hcraft.world import world_from_transformations
11 |
12 |
13 | class MiniCraftEnv(HcraftEnv):
14 | """Environments representing abstractions from
15 | [minigrid environments](https://minigrid.farama.org/environments/minigrid/).
16 | """
17 |
18 | MINICRAFT_NAME = None
19 |
20 | def __init__(
21 | self,
22 | minicraft_name: str,
23 | start_zone: Optional[Zone] = None,
24 | start_items: Optional[List[Union[Stack, Item]]] = None,
25 | start_zones_items: Optional[Dict[Zone, List[Union[Stack, Item]]]] = None,
26 | **kwargs,
27 | ) -> None:
28 | """
29 | Args:
30 | invalid_reward: Reward given to the agent for invalid actions.
31 | Defaults to -1.0.
32 | max_step: Maximum number of steps before episode truncation.
33 | If None, never truncates the episode. Defaults to None.
34 | render_window: Window using to render the environment with pygame.
35 | """
36 | self.MINICRAFT_NAME = minicraft_name
37 | transformations = self.build_transformations()
38 | world = world_from_transformations(
39 | transformations=transformations,
40 | start_zone=start_zone,
41 | start_items=start_items,
42 | start_zones_items=start_zones_items,
43 | )
44 | world.resources_path = Path(__file__).parent / "resources"
45 | super().__init__(world, name=f"MiniHCraft{self.MINICRAFT_NAME}", **kwargs)
46 |
47 | @abstractmethod
48 | def build_transformations(self) -> List[Transformation]:
49 | """Build transformations for this MiniCraft environment"""
50 | raise NotImplementedError
51 |
52 | @staticmethod
53 | def description(name: str, for_module_header: bool = False) -> str:
54 | """Docstring description of the MiniCraft environment."""
55 |
56 | minigrid_link = (
57 | "https://minigrid.farama.org/environments/minigrid/Env/"
58 | )
59 | minigrid_gif = "https://minigrid.farama.org/_images/Env.gif"
60 |
61 | doc_lines = [
62 | f"Reproduces the minigrid []({minigrid_link})"
63 | " gridworld environment as a HierarchyCraft environment.",
64 | ]
65 |
66 | if for_module_header:
67 | requirements_image = (
68 | "../../../../docs/images/requirements_graphs/MiniHCraft.html"
69 | )
70 |
71 | doc_lines = (
72 | ["# MiniCraft - ", "", ""]
73 | + doc_lines
74 | + [
75 | "",
76 | "Minigrid representation:",
77 | "",
78 | f'
',
79 | "",
80 | "HierarchyCraft requirements graph:",
81 | '',
82 | f".. include:: {requirements_image}",
83 | "
",
84 | ]
85 | )
86 |
87 | template = "\n".join(doc_lines)
88 | return template.replace("", name)
89 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
3 |
4 | [project]
5 | name = "hcraft"
6 | description = "Lightweight environments to study hierarchical reasoning"
7 |
8 | dynamic = ["version", "readme", "dependencies"]
9 | license = { text = "GNU General Public License v3 (GPLv3)" }
10 | requires-python = ">=3.8"
11 | authors = [
12 | { name = "Mathïs Fédérico" },
13 | { name = "Mathïs Fédérico", email = "mathfederico@gmail.com" },
14 | ]
15 | keywords = [
16 | "gym",
17 | "hcraft",
18 | "minecraft",
19 | "hierachical",
20 | "reinforcement",
21 | "learning",
22 | ]
23 |
24 |
25 | [project.optional-dependencies]
26 | gym = [
27 | "gymnasium>=1.0.0",
28 | ]
29 | gui = ["pygame >= 2.1.0", "pygame-menu >= 4.3.8"]
30 | planning = ["unified_planning[aries,enhsp] >= 1.1.0", "up-enhsp>=0.0.25"]
31 | htmlvis = ["pyvis<=0.3.1"]
32 | docs = [
33 | "pdoc>=14.7.0",
34 | ]
35 |
36 | [dependency-groups]
37 | dev = [
38 | "pytest",
39 | "pytest-cov",
40 | "pytest-mock",
41 | "pytest-check",
42 | "pytest-xdist",
43 | "pre-commit",
44 | "ruff"
45 | ]
46 |
47 | [project.urls]
48 | Source = "https://github.com/IRLL/HierarchyCraft"
49 |
50 | [tool.setuptools]
51 | license-files = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']
52 |
53 | [project.scripts]
54 | hcraft = "hcraft.__main__:main"
55 |
56 | [tool.setuptools.dynamic]
57 | readme = { file = ["README.md"] , content-type = "text/markdown"}
58 | dependencies = { file = ["requirements.txt"] }
59 |
60 | [tool.setuptools_scm]
61 |
62 | [tool.pytest.ini_options]
63 | markers = [
64 | "slow: marks tests as slow (deselect with '-m \"not slow\"')",
65 | ]
66 | testpaths = ["tests"]
67 | log_level = "DEBUG"
68 | filterwarnings = [
69 | 'ignore:pkg_resources is deprecated as an API:DeprecationWarning'
70 | ]
71 |
72 | [tool.coverage.run]
73 | source = ["src"]
74 |
75 | [tool.ruff]
76 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
77 | select = ["E", "F"]
78 | ignore = ["E501"]
79 |
80 | # Allow autofix for all enabled rules (when `--fix`) is provided.
81 | fixable = ["A", "B", "C", "D", "E", "F"]
82 | unfixable = []
83 |
84 | # Exclude a variety of commonly ignored directories.
85 | exclude = [
86 | ".bzr",
87 | ".direnv",
88 | ".eggs",
89 | ".git",
90 | ".hg",
91 | ".mypy_cache",
92 | ".nox",
93 | ".pants.d",
94 | ".pytype",
95 | ".ruff_cache",
96 | ".svn",
97 | ".tox",
98 | ".venv",
99 | "__pypackages__",
100 | "_build",
101 | "buck-out",
102 | "build",
103 | "dist",
104 | "node_modules",
105 | "venv",
106 | ]
107 |
108 | # Same as Black.
109 | line-length = 88
110 |
111 | # Allow unused variables when underscore-prefixed.
112 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
113 |
114 | # Assume Python 3.10.
115 | target-version = "py310"
116 |
117 | [tool.ruff.mccabe]
118 | # Unlike Flake8, default to a complexity level of 10.
119 | max-complexity = 10
120 |
121 | [tool.mypy]
122 | files = "src"
123 | plugins = "numpy.typing.mypy_plugin"
124 | check_untyped_defs = false
125 | disallow_any_generics = false
126 | disallow_incomplete_defs = true
127 | no_implicit_optional = false
128 | no_implicit_reexport = true
129 | strict_equality = true
130 | warn_redundant_casts = true
131 | warn_unused_ignores = true
132 | ignore_missing_imports = true
133 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/__init__.py:
--------------------------------------------------------------------------------
1 | """# MiniHCraft environments
2 |
3 | List of environments representing abstractions from
4 | [minigrid environments](https://minigrid.farama.org/environments/minigrid/).
5 |
6 | See submodules for each individual environement:
7 |
8 | | Minigrid name | Hcraft reference |
9 | |:--------------------|:------------------------------------------------|
10 | | Empty | `hcraft.examples.minicraft.empty` |
11 | | FourRooms | `hcraft.examples.minicraft.fourrooms` |
12 | | MultiRoom | `hcraft.examples.minicraft.multiroom` |
13 | | Crossing | `hcraft.examples.minicraft.crossing` |
14 | | KeyCorridor | `hcraft.examples.minicraft.keycorridor` |
15 | | DoorKey | `hcraft.examples.minicraft.doorkey` |
16 | | Unlock | `hcraft.examples.minicraft.unlock` |
17 | | UnlockPickup | `hcraft.examples.minicraft.unlockpickup` |
18 | | BlockedUnlockPickup | `hcraft.examples.minicraft.unlockpickupblocked` |
19 |
20 |
21 | """
22 |
23 | import inspect
24 | from pathlib import Path
25 | from typing import List, Type
26 |
27 |
28 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
29 |
30 | import hcraft.examples.minicraft.empty as empty
31 | import hcraft.examples.minicraft.fourrooms as fourrooms
32 | import hcraft.examples.minicraft.multiroom as multiroom
33 | import hcraft.examples.minicraft.crossing as crossing
34 | import hcraft.examples.minicraft.doorkey as doorkey
35 | import hcraft.examples.minicraft.unlock as unlock
36 | import hcraft.examples.minicraft.unlockpickup as unlockpickup
37 | import hcraft.examples.minicraft.unlockpickupblocked as unlockpickupblocked
38 | import hcraft.examples.minicraft.keycorridor as keycorridor
39 |
40 | from hcraft.examples.minicraft.empty import MiniHCraftEmpty
41 | from hcraft.examples.minicraft.fourrooms import MiniHCraftFourRooms
42 | from hcraft.examples.minicraft.multiroom import MiniHCraftMultiRoom
43 | from hcraft.examples.minicraft.crossing import MiniHCraftCrossing
44 | from hcraft.examples.minicraft.doorkey import MiniHCraftDoorKey
45 | from hcraft.examples.minicraft.unlock import MiniHCraftUnlock
46 | from hcraft.examples.minicraft.unlockpickup import MiniHCraftUnlockPickup
47 | from hcraft.examples.minicraft.unlockpickupblocked import MiniHCraftBlockedUnlockPickup
48 | from hcraft.examples.minicraft.keycorridor import MiniHCraftKeyCorridor
49 |
50 |
51 | MINICRAFT_ENVS: List[Type[MiniCraftEnv]] = [
52 | MiniHCraftEmpty,
53 | MiniHCraftFourRooms,
54 | MiniHCraftMultiRoom,
55 | MiniHCraftCrossing,
56 | MiniHCraftDoorKey,
57 | MiniHCraftUnlock,
58 | MiniHCraftUnlockPickup,
59 | MiniHCraftBlockedUnlockPickup,
60 | MiniHCraftKeyCorridor,
61 | ]
62 |
63 | MINICRAFT_NAME_TO_ENV = {env.MINICRAFT_NAME: env for env in MINICRAFT_ENVS}
64 |
65 | __all__ = [
66 | "empty",
67 | "fourrooms",
68 | "multiroom",
69 | "crossing",
70 | "doorkey",
71 | "unlock",
72 | "unlockpickup",
73 | "unlockpickupblocked",
74 | "keycorridor",
75 | ]
76 |
77 | MINICRAFT_GYM_ENVS = []
78 |
79 | try:
80 | import gymnasium as gym
81 |
82 | ENV_PATH = "hcraft.examples.minicraft"
83 |
84 | for env_name, env_class in MINICRAFT_NAME_TO_ENV.items():
85 | submodule = Path(inspect.getfile(env_class)).name.split(".")[0]
86 | env_path = f"{ENV_PATH}.{submodule}:{env_class.__name__}"
87 | gym_name = f"{env_name}-v1"
88 | gym.register(id=gym_name, entry_point=env_path)
89 | MINICRAFT_GYM_ENVS.append(gym_name)
90 |
91 |
92 | except ImportError:
93 | pass
94 |
--------------------------------------------------------------------------------
/tests/planning/test_mineHcraft.py:
--------------------------------------------------------------------------------
1 | """Module testing utils functions for hcraft behaviors."""
2 |
3 | from pathlib import Path
4 | from typing import TYPE_CHECKING
5 |
6 | import pytest
7 | import pytest_check as check
8 |
9 |
10 | from hcraft.elements import Item
11 | from hcraft.examples.minecraft.env import ALL_ITEMS, MineHcraftEnv
12 | from hcraft.examples.minecraft.tools import (
13 | MC_TOOLS_BY_TYPE_AND_MATERIAL,
14 | Material,
15 | ToolType,
16 | )
17 | from hcraft.task import GetItemTask
18 |
19 | if TYPE_CHECKING:
20 | from unified_planning.io import PDDLWriter
21 |
22 | WOODEN_PICKAXE = MC_TOOLS_BY_TYPE_AND_MATERIAL[ToolType.PICKAXE][Material.WOOD]
23 | STONE_PICKAXE = MC_TOOLS_BY_TYPE_AND_MATERIAL[ToolType.PICKAXE][Material.STONE]
24 |
25 |
26 | WOOD_LEVEL_ITEMS = [
27 | "wood_pickaxe",
28 | "wood_shovel",
29 | "wood_axe",
30 | "wood_sword",
31 | "cobblestone",
32 | "gravel",
33 | ]
34 |
35 | STONE_LEVEL_ITEMS = [
36 | "stone_pickaxe",
37 | "stone_sword",
38 | "stone_shovel",
39 | "stone_axe",
40 | "iron_ore",
41 | "furnace",
42 | "flint",
43 | "coal",
44 | ]
45 |
46 | LEATHER_TIER_ITEMS = [
47 | "leather",
48 | "book",
49 | ]
50 |
51 | IRON_TIER_ITEMS = [
52 | "iron_ingot",
53 | "flint_and_steel",
54 | "iron_axe",
55 | "iron_pickaxe",
56 | "iron_shovel",
57 | "iron_sword",
58 | ]
59 |
60 | DIAMOND_TIER_ITEMS = [
61 | "diamond",
62 | "gold_ore",
63 | "ender_pearl",
64 | "diamond_axe",
65 | "diamond_pickaxe",
66 | "diamond_shovel",
67 | "diamond_sword",
68 | "obsidian",
69 | "enchanting_table",
70 | ]
71 |
72 | GOLD_TIER_ITEMS = [
73 | "gold_ingot",
74 | "redstone",
75 | "clock",
76 | "gold_axe",
77 | "gold_pickaxe",
78 | "gold_shovel",
79 | "gold_sword",
80 | ]
81 |
82 | NETHER_LEVEL_ITEMS = [
83 | "netherrack",
84 | "blaze_rod",
85 | "blaze_powder",
86 | ]
87 |
88 | END_LEVEL_ITEMS = [
89 | "ender_eye",
90 | "ender_dragon_head",
91 | ]
92 |
93 | KNOWN_TO_FAIL_ITEM_FOR_PLANNER = {
94 | "enhsp": LEATHER_TIER_ITEMS
95 | + IRON_TIER_ITEMS
96 | + DIAMOND_TIER_ITEMS
97 | + GOLD_TIER_ITEMS
98 | + NETHER_LEVEL_ITEMS
99 | + END_LEVEL_ITEMS,
100 | "aries": WOOD_LEVEL_ITEMS
101 | + STONE_LEVEL_ITEMS
102 | + LEATHER_TIER_ITEMS
103 | + IRON_TIER_ITEMS
104 | + DIAMOND_TIER_ITEMS
105 | + GOLD_TIER_ITEMS
106 | + NETHER_LEVEL_ITEMS
107 | + END_LEVEL_ITEMS,
108 | }
109 |
110 |
111 | @pytest.mark.skip(reason="Known to be unstable or failing for most items")
112 | @pytest.mark.parametrize("item", [item.name for item in ALL_ITEMS])
113 | @pytest.mark.parametrize("planner_name", ["enhsp", "aries"])
114 | def test_get_item_flat(planner_name: str, item: str):
115 | """All items should be gettable by planning behavior."""
116 | up = pytest.importorskip("unified_planning")
117 | task = GetItemTask(Item(item))
118 | env = MineHcraftEnv(purpose=task, max_step=500)
119 | write = False
120 | problem = env.planning_problem(timeout=5, planner_name=planner_name)
121 |
122 | if write:
123 | writer: "PDDLWriter" = up.io.PDDLWriter(problem.upf_problem)
124 | pddl_dir = Path("planning", "pddl", env.name)
125 | pddl_dir.mkdir(exist_ok=True)
126 | writer.write_domain(pddl_dir / "MineHCraftDomain.pddl")
127 | writer.write_problem(pddl_dir / f"Get{item.capitalize()}Problem.pddl")
128 |
129 | optional_requirements = {"enhsp": "up_enhsp", "aries": "up_aries"}
130 | pytest.importorskip(optional_requirements[planner_name])
131 | if item in KNOWN_TO_FAIL_ITEM_FOR_PLANNER[planner_name]:
132 | pytest.xfail(f"{planner_name} planner is known to fail to get {item}")
133 |
134 | done = False
135 | _observation, _info = env.reset()
136 | while not done:
137 | action = problem.action_from_plan(env.state)
138 | _observation, _reward, terminated, truncated, _info = env.step(action)
139 | done = terminated or truncated
140 | check.is_true(terminated, msg=f"Plan failed :{problem.plans}")
141 | check.equal(env.current_step, len(problem.plans[0].actions))
142 |
--------------------------------------------------------------------------------
/src/hcraft/examples/random_simple/env.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=no-member
2 |
3 | """Random HierarchyCraft Environment
4 |
5 | Generate a random HierarchyCraft environment using basic constructor rules.
6 |
7 | """
8 |
9 | from typing import Dict, List, Optional
10 |
11 | import numpy as np
12 |
13 | from hcraft.elements import Item
14 | from hcraft.env import HcraftEnv
15 | from hcraft.transformation import Transformation, Use, Yield, PLAYER
16 | from hcraft.world import world_from_transformations
17 | from hcraft.purpose import GetItemTask, Purpose
18 |
19 |
20 | class RandomHcraftEnv(HcraftEnv):
21 | """Random HierarchyCraft Environment"""
22 |
23 | def __init__(
24 | self,
25 | n_items_per_n_inputs: Optional[Dict[int, int]] = None,
26 | seed: int = None,
27 | **kwargs,
28 | ):
29 | """Random HierarchyCraft Environment.
30 |
31 | Args:
32 | n_items_per_n_inputs: Mapping from the number of inputs to the number of items
33 | with this number of inputs.
34 | max_step: The maximum number of steps until done.
35 | """
36 | if n_items_per_n_inputs is None:
37 | n_items_per_n_inputs = {0: 5, 1: 5, 2: 10, 3: 5}
38 |
39 | self.seed = seed
40 | self.np_random = np.random.RandomState(seed)
41 | self.n_items = sum(n_items_per_n_inputs.values())
42 | env_characteristics = "".join(
43 | [
44 | f"{n_inputs}I{n_items}"
45 | for n_inputs, n_items in n_items_per_n_inputs.items()
46 | ]
47 | )
48 | name = f"RandomCrafing-{env_characteristics}-S{seed}"
49 | self.items: List[Item] = []
50 | transformations = self._transformations(n_items_per_n_inputs)
51 | world = world_from_transformations(transformations)
52 | if "purpose" not in kwargs:
53 | purpose = Purpose()
54 | for item in self.items:
55 | purpose.add_task(GetItemTask(item))
56 | kwargs["purpose"] = purpose
57 | super().__init__(world, name=name, **kwargs)
58 |
59 | def _transformations(
60 | self,
61 | n_items_per_n_inputs: Dict[int, int],
62 | ) -> List[Transformation]:
63 | """Build transformations for a RandomHcraft environement.
64 |
65 | Args:
66 | n_items_per_n_inputs: Mapping from the number of inputs to the number of items
67 | with this number of inputs.
68 | Returns:
69 | A list of random (but accessible) transformations.
70 |
71 | """
72 |
73 | for n_inputs, n_items in n_items_per_n_inputs.items():
74 | self.items += [Item(f"{n_inputs}_{i}") for i in range(n_items)]
75 |
76 | transformations = []
77 |
78 | # Items with 0 inputs are accessible from the start
79 | accessible_items = []
80 | for item in self.items:
81 | if item.name.startswith("0"):
82 | search_item = Transformation(inventory_changes=[Yield(PLAYER, item)])
83 | transformations.append(search_item)
84 | accessible_items.append(item)
85 |
86 | # Other items are built with inputs
87 | unaccessible_items = [
88 | item for item in self.items if item not in accessible_items
89 | ]
90 | self.np_random.shuffle(unaccessible_items)
91 |
92 | while len(accessible_items) < len(self.items):
93 | new_accessible_item = unaccessible_items.pop()
94 | inventory_changes = [Yield(PLAYER, new_accessible_item)]
95 |
96 | n_inputs = int(new_accessible_item.name.split("_")[0])
97 | n_inputs = min(n_inputs, len(accessible_items))
98 |
99 | # Chooses randomly accessible items
100 | input_items = list(
101 | self.np_random.choice(accessible_items, size=n_inputs, replace=False)
102 | )
103 | inventory_changes += [Use(PLAYER, item, consume=1) for item in input_items]
104 |
105 | # Build recipe
106 | new_recipe = Transformation(inventory_changes=inventory_changes)
107 | transformations.append(new_recipe)
108 | accessible_items.append(new_accessible_item)
109 |
110 | return transformations
111 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/doorkey.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 |
10 | MINICRAFT_NAME = "DoorKey"
11 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
12 |
13 |
14 | class MiniHCraftDoorKey(MiniCraftEnv):
15 | MINICRAFT_NAME = MINICRAFT_NAME
16 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
17 |
18 | START = Zone("start_room")
19 | """Start room."""
20 | LOCKED_ROOM = Zone("locked_room")
21 | """Room behind a locked door."""
22 |
23 | KEY = Item("key")
24 | """Key used to unlock the door."""
25 | GOAL = Item("goal")
26 | """Goal to reach."""
27 |
28 | OPEN_DOOR = Item("open_door")
29 | """Open door between the two rooms."""
30 | LOCKED_DOOR = Item("locked_door")
31 | """Locked door between the two rooms, can be unlocked with a key."""
32 |
33 | def __init__(self, **kwargs) -> None:
34 | self.task = GetItemTask(self.GOAL)
35 | super().__init__(
36 | self.MINICRAFT_NAME,
37 | purpose=self.task,
38 | start_zone=self.START,
39 | **kwargs,
40 | )
41 |
42 | def build_transformations(self) -> List[Transformation]:
43 | transformations = []
44 |
45 | # Ensure key cannot be created if anywhere else
46 | search_for_key = Transformation(
47 | "search_for_key",
48 | inventory_changes=[
49 | Yield(CURRENT_ZONE, self.KEY, create=1, max=0),
50 | Yield(PLAYER, self.KEY, create=0, max=0),
51 | Yield(self.START, self.KEY, create=0, max=0),
52 | Yield(self.LOCKED_ROOM, self.KEY, create=0, max=0),
53 | ],
54 | zone=self.START,
55 | )
56 | transformations.append(search_for_key)
57 |
58 | pickup = Transformation(
59 | "pickup_key",
60 | inventory_changes=[
61 | Use(CURRENT_ZONE, self.KEY, consume=1),
62 | Yield(PLAYER, self.KEY, create=1),
63 | ],
64 | )
65 | put_down = Transformation(
66 | "put_down_key",
67 | inventory_changes=[
68 | Use(PLAYER, self.KEY, consume=1),
69 | Yield(CURRENT_ZONE, self.KEY, create=1),
70 | ],
71 | )
72 | transformations += [pickup, put_down]
73 |
74 | search_for_door = Transformation(
75 | "search_for_door",
76 | inventory_changes=[
77 | Yield(CURRENT_ZONE, self.LOCKED_DOOR, max=0),
78 | Yield(CURRENT_ZONE, self.OPEN_DOOR, create=0, max=0),
79 | ],
80 | zone=self.START,
81 | )
82 | transformations.append(search_for_door)
83 |
84 | unlock_door = Transformation(
85 | "unlock_door",
86 | inventory_changes=[
87 | Use(PLAYER, self.KEY),
88 | Use(CURRENT_ZONE, self.LOCKED_DOOR, consume=1),
89 | Yield(CURRENT_ZONE, self.OPEN_DOOR, create=1),
90 | ],
91 | )
92 | transformations.append(unlock_door)
93 |
94 | move_to_locked_room = Transformation(
95 | "move_to_locked_room",
96 | destination=self.LOCKED_ROOM,
97 | inventory_changes=[Use(CURRENT_ZONE, self.OPEN_DOOR)],
98 | zone=self.START,
99 | )
100 | transformations.append(move_to_locked_room)
101 |
102 | move_to_start_room = Transformation(
103 | destination=self.START,
104 | zone=self.LOCKED_ROOM,
105 | )
106 | transformations.append(move_to_start_room)
107 |
108 | find_goal = Transformation(
109 | "find_goal",
110 | inventory_changes=[Yield(CURRENT_ZONE, self.GOAL, max=0)],
111 | zone=self.LOCKED_ROOM,
112 | )
113 | transformations.append(find_goal)
114 |
115 | reach_goal = Transformation(
116 | "reach_goal",
117 | inventory_changes=[
118 | Use(CURRENT_ZONE, self.GOAL, consume=1),
119 | Yield(PLAYER, self.GOAL),
120 | ],
121 | )
122 | transformations.append(reach_goal)
123 |
124 | return transformations
125 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/unlockpickup.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 |
8 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
9 |
10 |
11 | MINICRAFT_NAME = "UnlockPickup"
12 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
13 |
14 |
15 | class MiniHCraftUnlockPickup(MiniCraftEnv):
16 | MINICRAFT_NAME = MINICRAFT_NAME
17 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
18 |
19 | START = Zone("start_room")
20 | """Start room."""
21 | BOX_ROOM = Zone("box_room")
22 | """Room with a box inside."""
23 |
24 | KEY = Item("key")
25 | """Key used to unlock the door."""
26 | BOX = Item("box")
27 | """Box to pickup."""
28 | WEIGHT = Item("weight")
29 | """Weight of carried items, can only be less than 1."""
30 |
31 | OPEN_DOOR = Item("open_door")
32 | """Open door between the two rooms."""
33 | LOCKED_DOOR = Item("locked_door")
34 | """Locked door between the two rooms, can be unlocked with a key."""
35 |
36 | def __init__(self, **kwargs) -> None:
37 | self.task = GetItemTask(self.BOX)
38 | super().__init__(
39 | self.MINICRAFT_NAME,
40 | purpose=self.task,
41 | start_zone=self.START,
42 | **kwargs,
43 | )
44 |
45 | def build_transformations(self) -> List[Transformation]:
46 | transformations = []
47 |
48 | zones = (self.START, self.BOX_ROOM)
49 | items_in_zone = [(self.KEY, self.START), (self.BOX, self.BOX_ROOM)]
50 |
51 | for item, zone in items_in_zone:
52 | inventory_changes = [
53 | Yield(CURRENT_ZONE, item, create=1),
54 | # Prevent searching if already found
55 | Yield(PLAYER, item, create=0, max=0),
56 | ]
57 | # Prevent searching if item was placed elsewhere
58 | inventory_changes += [Yield(zone, item, create=0, max=0) for zone in zones]
59 | search_for_item = Transformation(
60 | f"search_for_{item.name}",
61 | inventory_changes=inventory_changes,
62 | zone=zone,
63 | )
64 | transformations.append(search_for_item)
65 |
66 | pickup = Transformation(
67 | f"pickup_{item.name}",
68 | inventory_changes=[
69 | Use(CURRENT_ZONE, item, consume=1),
70 | Yield(PLAYER, item, create=1),
71 | # WEIGHT prevents carrying more than one item
72 | Yield(PLAYER, self.WEIGHT, create=1, max=0),
73 | ],
74 | )
75 | put_down = Transformation(
76 | f"put_down_{item.name}",
77 | inventory_changes=[
78 | Use(PLAYER, item, consume=1),
79 | Yield(CURRENT_ZONE, item, create=1),
80 | # WEIGHT prevents carrying more than one item
81 | Use(PLAYER, self.WEIGHT, consume=1),
82 | ],
83 | )
84 | transformations += [pickup, put_down]
85 |
86 | search_for_door = Transformation(
87 | "search_for_door",
88 | inventory_changes=[
89 | Yield(CURRENT_ZONE, self.LOCKED_DOOR, max=0),
90 | Yield(CURRENT_ZONE, self.OPEN_DOOR, create=0, max=0),
91 | ],
92 | zone=self.START,
93 | )
94 | transformations.append(search_for_door)
95 |
96 | unlock_door = Transformation(
97 | "unlock_door",
98 | inventory_changes=[
99 | Use(PLAYER, self.KEY),
100 | Use(CURRENT_ZONE, self.LOCKED_DOOR, consume=1),
101 | Yield(CURRENT_ZONE, self.OPEN_DOOR),
102 | ],
103 | )
104 | transformations.append(unlock_door)
105 |
106 | move_to_box_room = Transformation(
107 | "move_to_box_room",
108 | destination=self.BOX_ROOM,
109 | inventory_changes=[Use(CURRENT_ZONE, self.OPEN_DOOR)],
110 | zone=self.START,
111 | )
112 | transformations.append(move_to_box_room)
113 |
114 | move_to_start_room = Transformation(
115 | "move_to_start_room",
116 | destination=self.START,
117 | zone=self.BOX_ROOM,
118 | )
119 | transformations.append(move_to_start_room)
120 |
121 | return transformations
122 |
--------------------------------------------------------------------------------
/src/hcraft/behaviors/feature_conditions.py:
--------------------------------------------------------------------------------
1 | """Module to define FeatureCondition nodes for the HEBGraph of the HierarchyCraft environment."""
2 |
3 | from typing import TYPE_CHECKING, Optional
4 |
5 | import numpy as np
6 | from hebg import FeatureCondition
7 |
8 | from hcraft.elements import Stack, Zone
9 | from hcraft.render.utils import load_or_create_image
10 | from hcraft.task import _quantity_str
11 |
12 | if TYPE_CHECKING:
13 | from hcraft.env import HcraftEnv
14 |
15 |
16 | class HasStack(FeatureCondition):
17 | """FeatureCondition to check if player has an Item in a given quantity."""
18 |
19 | def __init__(self, env: "HcraftEnv", stack: Stack) -> None:
20 | image = load_or_create_image(stack, env.world.resources_path)
21 | super().__init__(name=self.get_name(stack), image=np.array(image), complexity=1)
22 |
23 | self.stack = stack
24 | self.n_items = env.world.n_items
25 | self.slot = env.world.items.index(stack.item)
26 |
27 | @staticmethod
28 | def get_name(stack: Stack):
29 | """Name of the HasStack feature condition given the stack."""
30 | quantity_str = _quantity_str(stack.quantity)
31 | return f"Has{quantity_str}{stack.item.name}?"
32 |
33 | def __call__(self, observation) -> int:
34 | inventory_content = observation[: self.n_items]
35 | return inventory_content[self.slot] >= self.stack.quantity
36 |
37 |
38 | class HasLessStack(FeatureCondition):
39 | """FeatureCondition to check if player has an Item in less than a given quantity."""
40 |
41 | def __init__(self, env: "HcraftEnv", stack: Stack) -> None:
42 | image = load_or_create_image(stack, env.world.resources_path)
43 | super().__init__(name=self.get_name(stack), image=np.array(image), complexity=1)
44 |
45 | self.stack = stack
46 | self.n_items = env.world.n_items
47 | self.slot = env.world.items.index(stack.item)
48 |
49 | @staticmethod
50 | def get_name(stack: Stack):
51 | """Name of the HasStack feature condition given the stack."""
52 | return f"Has less than {stack.quantity} {stack.item.name}?"
53 |
54 | def __call__(self, observation) -> int:
55 | inventory_content = observation[: self.n_items]
56 | return inventory_content[self.slot] <= self.stack.quantity
57 |
58 |
59 | class IsInZone(FeatureCondition):
60 | """FeatureCondition to check if player is in a Zone."""
61 |
62 | def __init__(self, env: "HcraftEnv", zone: Zone) -> None:
63 | image = load_or_create_image(zone, env.world.resources_path)
64 | super().__init__(name=self.get_name(zone), image=image, complexity=1)
65 |
66 | self.n_items = env.world.n_items
67 | self.n_zones = env.world.n_zones
68 | self.zone = zone
69 | self.slot = env.world.slot_from_zone(zone)
70 |
71 | @staticmethod
72 | def get_name(zone: Zone):
73 | """Name of the IsInZone feature condition given the zone."""
74 | return f"Is in {zone.name}?"
75 |
76 | def __call__(self, observation) -> int:
77 | position = observation[self.n_items : self.n_items + self.n_zones]
78 | return position[self.slot] == 1
79 |
80 |
81 | class HasZoneItem(FeatureCondition):
82 | """FeatureCondition to check if a Zone has the given property."""
83 |
84 | def __init__(
85 | self, env: "HcraftEnv", stack: Stack, zone: Optional[Zone] = None
86 | ) -> None:
87 | image = np.array(load_or_create_image(stack, env.world.resources_path))
88 | super().__init__(name=self.get_name(stack, zone), image=image, complexity=1)
89 |
90 | self.stack = stack
91 | self.n_items = env.world.n_items
92 | self.n_zones = env.world.n_zones
93 | self.item_slot = env.world.zones_items.index(stack.item)
94 | self.zone_slot = env.world.zones.index(zone) if zone is not None else None
95 |
96 | # We cheat for now, we will deal with partial observability later.
97 | self.state = env.state
98 |
99 | @staticmethod
100 | def get_name(stack: Stack, zone: Optional[Zone] = None):
101 | """Name of the HasZoneItem feature condition given stack and optional zone."""
102 | zone_str = "Current zone" if zone is None else zone.name.capitalize()
103 | quantity_str = _quantity_str(stack.quantity)
104 | return f"{zone_str} has{quantity_str}{stack.item.name}?"
105 |
106 | def __call__(self, observation) -> int:
107 | if self.zone_slot is None:
108 | zone_items = observation[self.n_items + self.n_zones :]
109 | return zone_items[self.item_slot] >= self.stack.quantity
110 | return (
111 | self.state.zones_inventories[self.zone_slot, self.item_slot]
112 | >= self.stack.quantity
113 | )
114 |
--------------------------------------------------------------------------------
/src/hcraft/examples/tower.py:
--------------------------------------------------------------------------------
1 | """# TowerHcraft Environment
2 |
3 | Simple environment with tower-structured constructor rules
4 | to evaluate polynomial sub-behaviors reusability.
5 |
6 | The goal of the environment is to get the item on top of the tower.
7 |
8 | The tower has 'height' layers and 'width' items per layer,
9 | plus the final item on top of the last layer.
10 |
11 | Each item in the tower requires all the items of the previous layer to be built.
12 | Items of the floor layer require nothing and can be built from the start.
13 |
14 | ## Example
15 |
16 | For example here is a tower of height 2 and width 3:
17 |
18 | | | 6 | |
19 | |:-:|:-:|:-:|
20 | | 3 | 4 | 5 |
21 | | 0 | 1 | 2 |
22 |
23 | The goal here is to get the item 6.
24 | Item 6 requires the items {3, 4, 5}.
25 | Each of the items 3, 4 and 5 requires items {0, 1, 2}.
26 | Each of the items 0, 1 and 2 requires nothing and can be crafted from the start.
27 |
28 | Requirements graph for H2-W3:
29 |
30 | .. include:: ../../../docs/images/requirements_graphs/TowerHcraft-H2-W3.html
31 |
32 |
33 | """
34 |
35 | from typing import List
36 |
37 | from hcraft.elements import Item
38 | from hcraft.env import HcraftEnv
39 | from hcraft.transformation import Transformation, Use, Yield, PLAYER
40 | from hcraft.world import world_from_transformations
41 | from hcraft.task import GetItemTask
42 |
43 | try:
44 | import gymnasium as gym
45 |
46 | gym.register(
47 | id="TowerHcraft-v1",
48 | entry_point="hcraft.examples.tower:TowerHcraftEnv",
49 | )
50 |
51 | except ImportError:
52 | pass
53 |
54 |
55 | class TowerHcraftEnv(HcraftEnv):
56 | """Tower, a tower-structured hierarchical Environment.
57 |
58 | Item of given layer requires all items of the previous.
59 | The goal is to obtain the last item on top of the tower.
60 |
61 | """
62 |
63 | def __init__(self, height: int = 2, width: int = 3, **kwargs):
64 | """
65 | Args:
66 | height (int): Number of layers of the tower (ignoring goal item).
67 | width (int): Number of items per layer.
68 | """
69 | self.height = height
70 | self.width = width
71 | n_items = self.height * self.width + 1
72 | self.items = [Item(str(i)) for i in range(n_items)]
73 | name = f"TowerHcraft-H{self.height}-W{self.width}"
74 | if not isinstance(kwargs.get("max_step"), int):
75 | if self.width == 1:
76 | kwargs["max_step"] = 1 + int(self.width * (self.height + 1))
77 | else:
78 | # 1 + w + w**2 + ... + w**h
79 | kwargs["max_step"] = 1 + int(
80 | (1 - self.width ** (self.height + 1)) / (1 - self.width)
81 | )
82 | transformations = self.build_transformations(self.items)
83 | world = world_from_transformations(transformations)
84 | if "purpose" not in kwargs:
85 | kwargs["purpose"] = GetItemTask(self.items[-1])
86 | super().__init__(world, name=name, **kwargs)
87 |
88 | def build_transformations(self, items: List[Item]) -> List[Transformation]:
89 | """Build transformations to make every item accessible.
90 |
91 | Args:
92 | items: List of items.
93 |
94 | Returns:
95 | List of craft recipes as transformations.
96 |
97 | """
98 | transformations = []
99 |
100 | # First layer recipes
101 | for first_layer_id in range(self.width):
102 | item = items[first_layer_id]
103 | new_recipe = Transformation(inventory_changes=[Yield(PLAYER, item)])
104 | transformations.append(new_recipe)
105 |
106 | # Tower recipes
107 | for layer in range(1, self.height):
108 | for item_layer_id in range(self.width):
109 | item_id = layer * self.width + item_layer_id
110 | item = items[item_id]
111 |
112 | inventory_changes = [Yield(PLAYER, item)]
113 |
114 | prev_layer_id = (layer - 1) * self.width
115 | for prev_item_id in range(self.width):
116 | required_item = items[prev_layer_id + prev_item_id]
117 | inventory_changes.append(Use(PLAYER, required_item, consume=1))
118 |
119 | new_recipe = Transformation(inventory_changes=inventory_changes)
120 | transformations.append(new_recipe)
121 |
122 | # Last item recipe
123 | last_item = items[-1]
124 | inventory_changes = [Yield(PLAYER, last_item)]
125 | last_layer_id = (self.height - 1) * self.width
126 | for prev_item_id in range(self.width):
127 | required_item = items[last_layer_id + prev_item_id]
128 | inventory_changes.append(Use(PLAYER, required_item, consume=1))
129 |
130 | new_recipe = Transformation(inventory_changes=inventory_changes)
131 | transformations.append(new_recipe)
132 |
133 | return transformations
134 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/keycorridor.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 | MINICRAFT_NAME = "KeyCorridor"
10 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
11 |
12 |
13 | class MiniHCraftKeyCorridor(MiniCraftEnv):
14 | MINICRAFT_NAME = MINICRAFT_NAME
15 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
16 |
17 | START = Zone("start_room")
18 | """Start room."""
19 | KEY_ROOM = Zone("key_room")
20 | """Room containing a key."""
21 | LOCKED_ROOM = Zone("locked_room")
22 | """Room behind a locked door."""
23 |
24 | KEY = Item("key")
25 | """Key used to unlock locked door."""
26 | BALL = Item("ball")
27 | """Ball to pickup."""
28 | WEIGHT = Item("weight")
29 | """Weight of carried items."""
30 |
31 | OPEN_DOOR = Item("open_door")
32 | """Opened lockedroom door."""
33 | OPEN_KEY_DOOR = Item("blue_open_door")
34 | """Opened keyroom door."""
35 | CLOSED_KEY_DOOR = Item("blue_closed_door")
36 | """Closed keyroom door."""
37 | LOCKED_DOOR = Item("locked_door")
38 | """Locked door, can be unlocked with a key."""
39 |
40 | def __init__(self, **kwargs) -> None:
41 | self.task = GetItemTask(self.BALL)
42 | super().__init__(
43 | self.MINICRAFT_NAME,
44 | start_zone=self.START,
45 | start_zones_items={
46 | self.START: [self.CLOSED_KEY_DOOR, self.LOCKED_DOOR],
47 | self.LOCKED_ROOM: [self.BALL],
48 | },
49 | purpose=self.task,
50 | **kwargs,
51 | )
52 |
53 | def build_transformations(self) -> List[Transformation]:
54 | transformations = []
55 |
56 | # Ensure key cannot be created if anywhere else
57 | search_for_key = Transformation(
58 | "search_for_key",
59 | inventory_changes=[
60 | Yield(CURRENT_ZONE, self.KEY, create=1, max=0),
61 | Yield(PLAYER, self.KEY, create=0, max=0),
62 | Yield(self.START, self.KEY, create=0, max=0),
63 | Yield(self.KEY_ROOM, self.KEY, create=0, max=0),
64 | Yield(self.LOCKED_ROOM, self.KEY, create=0, max=0),
65 | ],
66 | zone=self.KEY_ROOM,
67 | )
68 | transformations.append(search_for_key)
69 |
70 | pickup = Transformation(
71 | "pickup_key",
72 | inventory_changes=[
73 | Use(CURRENT_ZONE, self.KEY, consume=1),
74 | Yield(PLAYER, self.KEY),
75 | Yield(PLAYER, self.WEIGHT, max=0),
76 | ],
77 | )
78 | put_down = Transformation(
79 | "put_down_key",
80 | inventory_changes=[
81 | Use(PLAYER, self.KEY, consume=1),
82 | Use(PLAYER, self.WEIGHT, consume=1),
83 | Yield(CURRENT_ZONE, self.KEY, create=1),
84 | ],
85 | )
86 | transformations += [pickup, put_down]
87 |
88 | open_door = Transformation(
89 | "open_door",
90 | inventory_changes=[
91 | Use(CURRENT_ZONE, self.CLOSED_KEY_DOOR, consume=1),
92 | Yield(CURRENT_ZONE, self.OPEN_KEY_DOOR, create=1),
93 | ],
94 | )
95 | transformations.append(open_door)
96 |
97 | move_to_key_room = Transformation(
98 | "move_to_key_room",
99 | destination=self.KEY_ROOM,
100 | inventory_changes=[Use(CURRENT_ZONE, self.OPEN_KEY_DOOR)],
101 | zone=self.START,
102 | )
103 | transformations.append(move_to_key_room)
104 |
105 | for from_zone in [self.KEY_ROOM, self.LOCKED_ROOM]:
106 | move_to_start_room = Transformation(
107 | f"move_to_start_room_from_{from_zone.name}",
108 | destination=self.START,
109 | zone=from_zone,
110 | )
111 | transformations.append(move_to_start_room)
112 |
113 | unlock_door = Transformation(
114 | "unlock_door",
115 | inventory_changes=[
116 | Use(PLAYER, self.KEY),
117 | Use(CURRENT_ZONE, self.LOCKED_DOOR, consume=1),
118 | Yield(CURRENT_ZONE, self.OPEN_DOOR, create=1),
119 | ],
120 | )
121 | transformations.append(unlock_door)
122 |
123 | move_to_locked_room = Transformation(
124 | "move_to_locked_room",
125 | destination=self.LOCKED_ROOM,
126 | inventory_changes=[Use(CURRENT_ZONE, self.OPEN_DOOR)],
127 | zone=self.START,
128 | )
129 | transformations.append(move_to_locked_room)
130 |
131 | pickup_ball = Transformation(
132 | "pickup_ball",
133 | inventory_changes=[
134 | Use(CURRENT_ZONE, self.BALL, consume=1),
135 | Yield(PLAYER, self.BALL),
136 | Yield(PLAYER, self.WEIGHT, max=0),
137 | ],
138 | )
139 | transformations.append(pickup_ball)
140 |
141 | return transformations
142 |
--------------------------------------------------------------------------------
/src/hcraft/examples/__init__.py:
--------------------------------------------------------------------------------
1 | """#HierarchyCraft environement examples.
2 |
3 | Here is the list of available HierarchyCraft environments examples.
4 |
5 | If you built one of your own, send us a pull request so we can add it to the list!
6 |
7 | ##Minecraft inspired
8 |
9 | See `hcraft.examples.minecraft` for more details.
10 | [CLI keyword: `minecraft`]
11 |
12 | | Gym name | Task description |
13 | |:---------------------------------|:-----------------------------------------------------------------------|
14 | | MineHcraft-NoReward-v1 | No task (Sandbox) |
15 | | MineHcraft-Stone-v1 | Get the cobblestone item mining it with a wooden pickaxe |
16 | | MineHcraft-Iron-v1 | Get the iron-ingot item smelting raw ore gathered with a stone pickage |
17 | | MineHcraft-Diamond-v1 | Get the diamond item mining it with an iron pickaxe |
18 | | MineHcraft-EnchantingTable-v1 | Craft the enchanting table from a book, obsidian and diamonds |
19 | | MineHcraft-Dragon-v1 | Get the ender-dragon-head by killing it in the ender |
20 | | MineHcraft-[name]-v1 | Get one of the Item of given `name` where Item is in env.world.items |
21 | | MineHcraft-v1 | Get all items at least once |
22 |
23 |
24 | ##Minigrid inspired
25 |
26 | [CLI keyword: `minicraft`]
27 |
28 | | Gym name | Documentation reference |
29 | |:---------------------------------|:------------------------------------------------|
30 | | MiniHCraftEmpty-v1 | `hcraft.examples.minicraft.empty` |
31 | | MiniHCraftFourRooms-v1 | `hcraft.examples.minicraft.fourrooms` |
32 | | MiniHCraftMultiRoom-v1 | `hcraft.examples.minicraft.multiroom` |
33 | | MiniHCraftCrossing-v1 | `hcraft.examples.minicraft.crossing` |
34 | | MiniHCraftKeyCorridor-v1 | `hcraft.examples.minicraft.keycorridor` |
35 | | MiniHCraftDoorKey-v1 | `hcraft.examples.minicraft.doorkey` |
36 | | MiniHCraftUnlock-v1 | `hcraft.examples.minicraft.unlock` |
37 | | MiniHCraftUnlockPickup-v1 | `hcraft.examples.minicraft.unlockpickup` |
38 | | MiniHCraftBlockedUnlockPickup-v1 | `hcraft.examples.minicraft.unlockpickupblocked` |
39 |
40 | ##Parametrised toy structures
41 | | Gym name | CLI name | Reference |
42 | |:---------------------------------|:------------------|:------------------------------------------------|
43 | | TowerHcraft-v1 | `tower` | `hcraft.examples.tower` |
44 | | RecursiveHcraft-v1 | `recursive` | `hcraft.examples.recursive` |
45 | | LightRecursiveHcraft-v1 | `light-recursive` | `hcraft.examples.light_recursive` |
46 |
47 | ##Stochastic parametrised toy structures
48 | | Gym name | CLI name | Reference |
49 | |:---------------------------------|:------------------|:------------------------------------------------|
50 | | RandomHcraft-v1 | `random` | `hcraft.examples.random_simple` |
51 |
52 | ##Other examples
53 | | Gym name | CLI name | Reference |
54 | |:---------------------------------|:------------------|:------------------------------------------------|
55 | | Treasure-v1 | `treasure` | `hcraft.examples.treasure` |
56 |
57 |
58 | """
59 |
60 | import hcraft.examples.minecraft as minecraft
61 | import hcraft.examples.minicraft as minicraft
62 | import hcraft.examples.random_simple as random_simple
63 | import hcraft.examples.recursive as recursive
64 | import hcraft.examples.light_recursive as light_recursive
65 | import hcraft.examples.tower as tower
66 | import hcraft.examples.treasure as treasure
67 |
68 |
69 | from hcraft.examples.minecraft import MineHcraftEnv, MINEHCRAFT_GYM_ENVS
70 | from hcraft.examples.minicraft import MINICRAFT_ENVS, MINICRAFT_GYM_ENVS
71 | from hcraft.examples.recursive import RecursiveHcraftEnv
72 | from hcraft.examples.light_recursive import LightRecursiveHcraftEnv
73 | from hcraft.examples.tower import TowerHcraftEnv
74 | from hcraft.examples.treasure import TreasureEnv
75 | from hcraft.examples.random_simple import RandomHcraftEnv
76 |
77 | EXAMPLE_ENVS = [
78 | MineHcraftEnv,
79 | *MINICRAFT_ENVS,
80 | TowerHcraftEnv,
81 | RecursiveHcraftEnv,
82 | LightRecursiveHcraftEnv,
83 | TreasureEnv,
84 | # RandomHcraftEnv,
85 | ]
86 |
87 | HCRAFT_GYM_ENVS = [
88 | *MINEHCRAFT_GYM_ENVS,
89 | *MINICRAFT_GYM_ENVS,
90 | "TowerHcraft-v1",
91 | "RecursiveHcraft-v1",
92 | "LightRecursiveHcraft-v1",
93 | "Treasure-v1",
94 | ]
95 |
96 |
97 | __all__ = [
98 | "minecraft",
99 | "minicraft",
100 | "recursive",
101 | "light_recursive",
102 | "tower",
103 | "treasure",
104 | "random_simple",
105 | "MineHcraftEnv",
106 | "RandomHcraftEnv",
107 | "LightRecursiveHcraftEnv",
108 | "RecursiveHcraftEnv",
109 | "TowerHcraftEnv",
110 | ]
111 |
--------------------------------------------------------------------------------
/docs/template/layout.css:
--------------------------------------------------------------------------------
1 | /*
2 | This CSS file contains all style definitions for the global page layout.
3 |
4 | When pdoc is embedded into other systems, it may be left out (or overwritten with an empty file) entirely.
5 | */
6 |
7 | /* Responsive Layout */
8 | html, body {
9 | width: 100%;
10 | height: 100%;
11 | }
12 |
13 | html, main {
14 | scroll-behavior: smooth;
15 | }
16 |
17 | body {
18 | background-color: var(--pdoc-background);
19 | }
20 |
21 | @media (max-width: 769px) {
22 | #navtoggle {
23 | cursor: pointer;
24 | position: absolute;
25 | width: 50px;
26 | height: 40px;
27 | top: 1rem;
28 | right: 1rem;
29 | border-color: var(--text);
30 | color: var(--text);
31 | display: flex;
32 | opacity: 0.8;
33 | }
34 |
35 | #navtoggle:hover {
36 | opacity: 1;
37 | }
38 |
39 | #togglestate + div {
40 | display: none;
41 | }
42 |
43 | #togglestate:checked + div {
44 | display: inherit;
45 | }
46 |
47 | main, header {
48 | padding: 2rem 3vw;
49 | }
50 |
51 | header + main {
52 | margin-top: -3rem;
53 | }
54 |
55 | .git-button {
56 | display: none !important;
57 | }
58 |
59 | nav input[type="search"] {
60 | /* don't overflow into menu button */
61 | max-width: 77%;
62 | }
63 |
64 | nav input[type="search"]:first-child {
65 | /* align vertically with the hamburger menu */
66 | margin-top: -6px;
67 | }
68 |
69 | nav input[type="search"]:valid ~ * {
70 | /* hide rest of the menu when search has contents */
71 | display: none !important;
72 | }
73 | }
74 |
75 | @media (min-width: 770px) {
76 | :root {
77 | --sidebar-width: clamp(12.5rem, 28vw, 22rem);
78 | }
79 |
80 | nav {
81 | position: fixed;
82 | overflow: auto;
83 | height: 100vh;
84 | width: var(--sidebar-width);
85 | }
86 |
87 | main, header {
88 | padding: 3rem 2rem 3rem calc(var(--sidebar-width) + 3rem);
89 | width: calc(54rem + var(--sidebar-width));
90 | max-width: 100%;
91 | }
92 |
93 | header + main {
94 | margin-top: -4rem;
95 | }
96 |
97 | #navtoggle {
98 | display: none;
99 | }
100 | }
101 |
102 | #togglestate {
103 | /*
104 | Don't do `display: none` here.
105 | When a mobile browser is not scrolled all the way to the top,
106 | clicking the label would insert the menu above the scrolling position
107 | and it would stay out of view. By making the checkbox technically
108 | visible it jumps up first and we always get the menu into view when clicked.
109 | */
110 | position: absolute;
111 | height: 0;
112 | /* height:0 isn't enough in Firefox, so we hide it extra well */
113 | opacity: 0;
114 | }
115 |
116 | /* Nav */
117 | nav.pdoc {
118 | --pad: clamp(0.5rem, 2vw, 1.75rem);
119 | --indent: 1.5rem;
120 | background-color: var(--nav-background);
121 | border-right: 1px solid var(--nav-border);
122 | box-shadow: 0 0 20px rgba(50, 50, 50, .2) inset;
123 | padding: 0 0 0 var(--pad);
124 | overflow-wrap: anywhere;
125 | scrollbar-width: thin; /* Scrollbar width on Firefox */
126 | scrollbar-color: var(--nav-border) transparent /* Scrollbar color on Firefox */
127 | }
128 |
129 | nav.pdoc::-webkit-scrollbar {
130 | width: .4rem; /* Scrollbar width on Chromium-based browsers */
131 | }
132 |
133 | nav.pdoc::-webkit-scrollbar-thumb {
134 | background-color: var(--accent2); /* Scrollbar color on Chromium-based browsers */
135 | }
136 |
137 | nav.pdoc > div {
138 | padding: var(--pad) 0;
139 | }
140 |
141 | nav.pdoc .module-list-button {
142 | display: inline-flex;
143 | align-items: center;
144 | color: var(--nav-text);
145 | border-color: var(--muted);
146 | margin-bottom: 1rem;
147 | }
148 |
149 | nav.pdoc .module-list-button:hover {
150 | border-color: var(--text);
151 | }
152 |
153 | nav.pdoc input[type=search] {
154 | display: block;
155 | outline-offset: 0;
156 | width: calc(100% - var(--pad));
157 | }
158 |
159 | nav.pdoc .logo {
160 | max-width: calc(100% - var(--pad));
161 | max-height: 35vh;
162 | display: block;
163 | margin: 0 auto 1rem;
164 | transform: translate(calc(-.5 * var(--pad)), 0);
165 | }
166 |
167 | nav.pdoc ul {
168 | list-style: none;
169 | padding-left: 0;
170 | }
171 |
172 | nav.pdoc > div > ul {
173 | /* undo padding here so that links span entire width */
174 | margin-left: calc(0px - var(--pad));
175 | }
176 |
177 | nav.pdoc li a {
178 | /* re-add padding (+indent) here */
179 | padding: .2rem 0 .2rem calc(var(--pad) + var(--indent));
180 | }
181 |
182 | nav.pdoc > div > ul > li > a {
183 | /* no padding for top-level */
184 | padding-left: var(--pad);
185 | }
186 |
187 | nav.pdoc li {
188 | transition: all 100ms;
189 | }
190 |
191 | nav.pdoc a, nav.pdoc {
192 | color: var(--nav-text);
193 | }
194 | nav.pdoc a:hover {
195 | background-color: var(--nav-hover);
196 | }
197 |
198 | nav.pdoc a {
199 | display: block;
200 | }
201 |
202 | nav.pdoc > h2:first-of-type {
203 | margin-top: 1.5rem;
204 | }
205 |
206 | nav.pdoc .class:before {
207 | content: "class ";
208 | color: var(--muted);
209 | }
210 |
211 | nav.pdoc .function:after {
212 | content: "()";
213 | color: var(--muted);
214 | }
215 |
216 | nav.pdoc footer:before {
217 | content: "";
218 | display: block;
219 | width: calc(100% - var(--pad));
220 | border-top: solid var(--accent2) 1px;
221 | margin-top: 1.5rem;
222 | padding-top: .5rem;
223 | }
224 |
225 | nav.pdoc footer {
226 | font-size: small;
227 | }
228 |
--------------------------------------------------------------------------------
/src/hcraft/solving_behaviors.py:
--------------------------------------------------------------------------------
1 | """# Solving behaviors
2 |
3 | HierarchyCraft environments comes with built-in solutions.
4 | For ANY task in ANY HierarchyCraft environment, a solving behavior can be given
5 | thanks to the fact that no feature extraction is required.
6 |
7 | This behavior can be called on the observation and will return relevant actions, like any agent.
8 |
9 | Solving behavior for any task can simply be obtained like this:
10 |
11 | ```python
12 | behavior = env.solving_behavior(task)
13 | action = behavior(observation)
14 | ```
15 |
16 | Solving behaviors can be used for imitation learning, as teacher or an expert policy.
17 |
18 | ## Example
19 |
20 | Let's get a DIAMOND in MineHcraft:
21 |
22 | ```python
23 | from hcraft.examples import MineHcraftEnv
24 | from hcraft.examples.minecraft.items import DIAMOND
25 | from hcraft.task import GetItemTask
26 |
27 | get_diamond = GetItemTask(DIAMOND)
28 | env = MineHcraftEnv(purpose=get_diamond)
29 | solving_behavior = env.solving_behavior(get_diamond)
30 |
31 | done = False
32 | observation, _info = env.reset()
33 | while not done:
34 | action = solving_behavior(observation)
35 | observation, _reward, terminated, truncated, _info = env.step(action)
36 | done = terminated or truncated
37 |
38 | assert terminated # Env is successfuly terminated
39 | assert get_diamond.is_terminated # DIAMOND has been obtained !
40 | ```
41 |
42 |
43 | """
44 |
45 | from typing import TYPE_CHECKING, Dict
46 |
47 | from hebg import Behavior
48 |
49 | from hcraft.behaviors.behaviors import (
50 | AbleAndPerformTransformation,
51 | GetItem,
52 | DropItem,
53 | PlaceItem,
54 | ReachZone,
55 | )
56 | from hcraft.requirements import RequirementNode, req_node_name
57 | from hcraft.task import GetItemTask, GoToZoneTask, PlaceItemTask, Task
58 |
59 |
60 | if TYPE_CHECKING:
61 | from hcraft.env import HcraftEnv
62 |
63 |
64 | def build_all_solving_behaviors(env: "HcraftEnv") -> Dict[str, "Behavior"]:
65 | """Return a dictionary of handcrafted behaviors to get each item, zone and property."""
66 | all_behaviors = {}
67 | all_behaviors = _reach_zones_behaviors(env, all_behaviors)
68 | all_behaviors = _get_item_behaviors(env, all_behaviors)
69 | all_behaviors = _drop_item_behaviors(env, all_behaviors)
70 | all_behaviors = _get_zone_item_behaviors(env, all_behaviors)
71 | all_behaviors = _do_transfo_behaviors(env, all_behaviors)
72 |
73 | empty_behaviors = []
74 | for name, behavior in all_behaviors.items():
75 | try:
76 | behavior.graph
77 | except ValueError:
78 | empty_behaviors.append(name)
79 | for name in empty_behaviors:
80 | all_behaviors.pop(name)
81 |
82 | # TODO: Use learning complexity instead for more generality
83 | requirements_graph = env.world.requirements.graph
84 |
85 | for behavior in all_behaviors.values():
86 | if isinstance(behavior, AbleAndPerformTransformation):
87 | behavior.complexity = 1
88 | continue
89 | if isinstance(behavior, GetItem):
90 | req_node = req_node_name(behavior.item, RequirementNode.ITEM)
91 | elif isinstance(behavior, DropItem):
92 | # TODO: this clearly is not general enough,
93 | # it would need requirements for non-accumulative to be fine
94 | req_node = req_node_name(behavior.item, RequirementNode.ITEM)
95 | elif isinstance(behavior, ReachZone):
96 | req_node = req_node_name(behavior.zone, RequirementNode.ZONE)
97 | elif isinstance(behavior, PlaceItem):
98 | req_node = req_node_name(behavior.item, RequirementNode.ZONE_ITEM)
99 | else:
100 | raise NotImplementedError
101 | behavior.complexity = requirements_graph.nodes[req_node]["level"]
102 | continue
103 |
104 | return all_behaviors
105 |
106 |
107 | def task_to_behavior_name(task: Task) -> str:
108 | """Get the behavior name that will solve the given task.
109 |
110 | Args:
111 | task: Task to be solved.
112 |
113 | Raises:
114 | NotImplementedError: If task is not supported yet.
115 |
116 | Returns:
117 | str: Name of the solving behavior.
118 | """
119 | if isinstance(task, GetItemTask):
120 | behavior_name = GetItem.get_name(task.item_stack.item)
121 | elif isinstance(task, GoToZoneTask):
122 | behavior_name = ReachZone.get_name(task.zone)
123 | elif isinstance(task, PlaceItemTask):
124 | behavior_name = PlaceItem.get_name(task.item_stack.item, task.zone)
125 | else:
126 | raise NotImplementedError
127 | return behavior_name
128 |
129 |
130 | def _reach_zones_behaviors(env: "HcraftEnv", all_behaviors: Dict[str, "Behavior"]):
131 | for zone in env.world.zones:
132 | behavior = ReachZone(zone, env, all_behaviors=all_behaviors)
133 | all_behaviors[behavior.name] = behavior
134 | return all_behaviors
135 |
136 |
137 | def _get_item_behaviors(env: "HcraftEnv", all_behaviors: Dict[str, "Behavior"]):
138 | for item in env.world.items:
139 | behavior = GetItem(item, env, all_behaviors=all_behaviors)
140 | all_behaviors[behavior.name] = behavior
141 | return all_behaviors
142 |
143 |
144 | def _drop_item_behaviors(env: "HcraftEnv", all_behaviors: Dict[str, "Behavior"]):
145 | for item in env.world.items:
146 | behavior = DropItem(item, env, all_behaviors=all_behaviors)
147 | all_behaviors[behavior.name] = behavior
148 | return all_behaviors
149 |
150 |
151 | def _get_zone_item_behaviors(env: "HcraftEnv", all_behaviors: Dict[str, "Behavior"]):
152 | for zone in [None] + env.world.zones: # Anywhere + in every specific zone
153 | for item in env.world.zones_items:
154 | behavior = PlaceItem(item, env, all_behaviors=all_behaviors, zone=zone)
155 | all_behaviors[behavior.name] = behavior
156 | return all_behaviors
157 |
158 |
159 | def _do_transfo_behaviors(env: "HcraftEnv", all_behaviors: Dict[str, "Behavior"]):
160 | for transfo in env.world.transformations:
161 | behavior = AbleAndPerformTransformation(
162 | env, transfo, all_behaviors=all_behaviors
163 | )
164 | all_behaviors[behavior.name] = behavior
165 | return all_behaviors
166 |
--------------------------------------------------------------------------------
/src/hcraft/task.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 | from typing import TYPE_CHECKING, List, Optional, Union
3 |
4 | import numpy as np
5 |
6 | from hcraft.elements import Item, Stack, Zone
7 |
8 | if TYPE_CHECKING:
9 | from hcraft.env import HcraftState
10 | from hcraft.world import World
11 |
12 |
13 | class Task:
14 | """Abstract base class for all HierarchyCraft tasks."""
15 |
16 | def __init__(self, name: str) -> None:
17 | self.name = name
18 | self.terminated = False
19 | self._terminate_player_items = None
20 | self._terminate_position = None
21 | self._terminate_zones_items = None
22 |
23 | def build(self, world: "World") -> None:
24 | """Build the task operation arrays based on the given world."""
25 | self._terminate_position = np.zeros(world.n_zones, dtype=np.int32)
26 | self._terminate_player_items = np.zeros(world.n_items, dtype=np.int32)
27 | self._terminate_zones_items = np.zeros(
28 | (world.n_zones, world.n_zones_items), dtype=np.int32
29 | )
30 |
31 | def is_terminal(self, state: "HcraftState") -> bool:
32 | """
33 | Returns whether the task is terminated.
34 | """
35 | if self.terminated:
36 | return True
37 | self.terminated = self._is_terminal(state)
38 | return self.terminated
39 |
40 | @abstractmethod
41 | def _is_terminal(self, state: "HcraftState") -> bool:
42 | """"""
43 |
44 | @abstractmethod
45 | def reward(self, state: "HcraftState") -> float:
46 | """
47 | Returns the reward for the given state.
48 | """
49 |
50 | def reset(self) -> None:
51 | """
52 | Reset the task termination.
53 | """
54 | self.terminated = False
55 |
56 | def __str__(self) -> str:
57 | return self.name
58 |
59 | def __repr__(self) -> str:
60 | return self.name
61 |
62 |
63 | class AchievementTask(Task):
64 | """Task giving a reward to the player only the first time achieved."""
65 |
66 | def __init__(self, name: str, reward: float):
67 | super().__init__(name)
68 | self._reward = reward
69 |
70 | @abstractmethod
71 | def _is_terminal(self, state: "HcraftState") -> bool:
72 | """
73 | Returns when the achievement is completed.
74 | """
75 |
76 | def reward(self, state: "HcraftState") -> float:
77 | if not self.terminated and self._is_terminal(state):
78 | return self._reward
79 | return 0.0
80 |
81 |
82 | class GetItemTask(AchievementTask):
83 | """Task of getting a given quantity of an item."""
84 |
85 | def __init__(self, item_stack: Union[Item, Stack], reward: float = 1.0):
86 | self.item_stack = _stack_item(item_stack)
87 | super().__init__(name=self.get_name(self.item_stack), reward=reward)
88 |
89 | def build(self, world: "World") -> None:
90 | super().build(world)
91 | item_slot = world.items.index(self.item_stack.item)
92 | self._terminate_player_items[item_slot] = self.item_stack.quantity
93 |
94 | def _is_terminal(self, state: "HcraftState") -> bool:
95 | return np.all(state.player_inventory >= self._terminate_player_items)
96 |
97 | @staticmethod
98 | def get_name(stack: Stack):
99 | """Name of the task for a given Stack"""
100 | quantity_str = _quantity_str(stack.quantity)
101 | return f"Get{quantity_str}{stack.item.name}"
102 |
103 |
104 | class GoToZoneTask(AchievementTask):
105 | """Task to go to a given zone."""
106 |
107 | def __init__(self, zone: Zone, reward: float = 1.0) -> None:
108 | super().__init__(name=self.get_name(zone), reward=reward)
109 | self.zone = zone
110 |
111 | def build(self, world: "World"):
112 | super().build(world)
113 | zone_slot = world.zones.index(self.zone)
114 | self._terminate_position[zone_slot] = 1
115 |
116 | def _is_terminal(self, state: "HcraftState") -> bool:
117 | return np.all(state.position == self._terminate_position)
118 |
119 | @staticmethod
120 | def get_name(zone: Zone):
121 | """Name of the task for a given Stack"""
122 | return f"Go to {zone.name}"
123 |
124 |
125 | class PlaceItemTask(AchievementTask):
126 | """Task to place a quantity of item in a given zone.
127 |
128 | If no zone is given, consider placing the item anywhere.
129 |
130 | """
131 |
132 | def __init__(
133 | self,
134 | item_stack: Union[Item, Stack],
135 | zone: Optional[Union[Zone, List[Zone]]] = None,
136 | reward: float = 1.0,
137 | ):
138 | item_stack = _stack_item(item_stack)
139 | self.item_stack = item_stack
140 | self.zone = zone
141 | super().__init__(name=self.get_name(item_stack, zone), reward=reward)
142 |
143 | def build(self, world: "World"):
144 | super().build(world)
145 | if self.zone is None:
146 | zones_slots = np.arange(self._terminate_zones_items.shape[0])
147 | else:
148 | zones_slots = np.array([world.slot_from_zone(self.zone)])
149 | zone_item_slot = world.zones_items.index(self.item_stack.item)
150 | self._terminate_zones_items[zones_slots, zone_item_slot] = (
151 | self.item_stack.quantity
152 | )
153 |
154 | def _is_terminal(self, state: "HcraftState") -> bool:
155 | if self.zone is None:
156 | return np.any(
157 | np.all(state.zones_inventories >= self._terminate_zones_items, axis=1)
158 | )
159 | return np.all(state.zones_inventories >= self._terminate_zones_items)
160 |
161 | @staticmethod
162 | def get_name(stack: Stack, zone: Optional[Zone]):
163 | """Name of the task for a given Stack and list of Zone"""
164 | quantity_str = _quantity_str(stack.quantity)
165 | zones_str = _zones_str(zone)
166 | return f"Place{quantity_str}{stack.item.name}{zones_str}"
167 |
168 |
169 | def _stack_item(item_or_stack: Union[Item, Stack]) -> Stack:
170 | if not isinstance(item_or_stack, Stack):
171 | item_or_stack = Stack(item_or_stack)
172 | return item_or_stack
173 |
174 |
175 | def _quantity_str(quantity: int):
176 | return f" {quantity} " if quantity > 1 else " "
177 |
178 |
179 | def _zones_str(zone: Optional[Zone]):
180 | if zone is None:
181 | return " anywhere"
182 | return f" in {zone.name}"
183 |
--------------------------------------------------------------------------------
/tests/test_tasks.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from typing import Any
3 |
4 | import numpy as np
5 | import pytest
6 | import pytest_check as check
7 |
8 | from hcraft.elements import Item, Stack, Zone
9 | from hcraft.task import GetItemTask, GoToZoneTask, PlaceItemTask
10 | from hcraft.world import World
11 | from tests.custom_checks import check_np_equal
12 |
13 |
14 | @dataclass
15 | class DummyState:
16 | player_inventory: Any = None
17 | position: Any = None
18 | zones_inventories: Any = None
19 |
20 |
21 | def simple_world() -> World:
22 | return World(
23 | items=[Item("dirt"), Item("wood"), Item("stone"), Item("plank")],
24 | zones=[Zone("start"), Zone("other_zone")],
25 | zones_items=[Item("dirt"), Item("table"), Item("wood_house")],
26 | transformations=[],
27 | )
28 |
29 |
30 | class TestGetItem:
31 | @pytest.fixture(autouse=True)
32 | def setup_method(self):
33 | self.world = simple_world()
34 | self.task = GetItemTask(Stack(Item("wood"), 3), reward=5)
35 |
36 | def test_build(self):
37 | """should build expected operations arrays based on the given world."""
38 | self.task.build(self.world)
39 | check_np_equal(self.task._terminate_player_items, np.array([0, 3, 0, 0]))
40 |
41 | def test_build_with_item_only(self):
42 | """should build even if the item is given without a stack."""
43 | task = GetItemTask(Item("wood"), reward=5)
44 | task.build(self.world)
45 | check_np_equal(task._terminate_player_items, np.array([0, 1, 0, 0]))
46 |
47 | def test_terminate(self):
48 | """should terminate only when the player has more than wanted items."""
49 | self.task.build(self.world)
50 |
51 | state = DummyState(player_inventory=np.array([10, 2, 10, 10]))
52 | check.is_false(self.task.is_terminal(state))
53 | state = DummyState(player_inventory=np.array([0, 3, 0, 0]))
54 | check.is_true(self.task.is_terminal(state))
55 | state = DummyState(player_inventory=np.array([0, 4, 0, 0]))
56 | check.is_true(self.task.is_terminal(state))
57 |
58 | def test_reward(self):
59 | """should reward only the first time the task terminates."""
60 | self.task.build(self.world)
61 |
62 | state = DummyState(player_inventory=np.array([10, 2, 10, 10]))
63 | check.equal(self.task.reward(state), 0)
64 | state = DummyState(player_inventory=np.array([0, 4, 0, 0]))
65 | check.equal(self.task.reward(state), 5)
66 | self.task.terminated = True
67 | check.equal(self.task.reward(state), 0)
68 |
69 |
70 | class TestGoToZone:
71 | @pytest.fixture(autouse=True)
72 | def setup_method(self):
73 | self.world = simple_world()
74 | self.task = GoToZoneTask(Zone("other_zone"), reward=5)
75 |
76 | def test_build(self):
77 | """should build expected operations arrays based on the given world."""
78 | self.task.build(self.world)
79 | check_np_equal(self.task._terminate_position, np.array([0, 1]))
80 |
81 | def test_terminate(self):
82 | """should terminate only when the player is in the zone"""
83 | self.task.build(self.world)
84 |
85 | state = DummyState(position=np.array([1, 0]))
86 | check.is_false(self.task.is_terminal(state))
87 | state = DummyState(position=np.array([0, 1]))
88 | check.is_true(self.task.is_terminal(state))
89 |
90 | def test_reward(self):
91 | """should reward only the first time the task terminates."""
92 | self.task.build(self.world)
93 |
94 | state = DummyState(position=np.array([1, 0]))
95 | check.equal(self.task.reward(state), 0)
96 | state = DummyState(position=np.array([0, 1]))
97 | check.equal(self.task.reward(state), 5)
98 | self.task.terminated = True
99 | check.equal(self.task.reward(state), 0)
100 |
101 |
102 | class TestPlaceItem:
103 | @pytest.fixture(autouse=True)
104 | def setup_method(self):
105 | self.world = simple_world()
106 | self.task = PlaceItemTask(
107 | Stack(Item("wood_house"), 2), Zone("other_zone"), reward=5
108 | )
109 |
110 | def test_build(self):
111 | """should build expected operations arrays based on the given world."""
112 | self.task.build(self.world)
113 | expected_op = np.array([[0, 0, 0], [0, 0, 2]])
114 | check_np_equal(self.task._terminate_zones_items, expected_op)
115 |
116 | def test_build_with_item_only(self):
117 | """should build even if the item is given without a stack."""
118 | task = PlaceItemTask(Item("wood_house"), Zone("other_zone"), reward=5)
119 | task.build(self.world)
120 | expected_op = np.array([[0, 0, 0], [0, 0, 1]])
121 | check_np_equal(task._terminate_zones_items, expected_op)
122 |
123 | def test_build_no_zone(self):
124 | """should consider any zone if none is given."""
125 | task = PlaceItemTask(Stack(Item("wood_house"), 2), None, reward=5)
126 | task.build(self.world)
127 | expected_op = np.array([[0, 0, 2], [0, 0, 2]])
128 | check_np_equal(task._terminate_zones_items, expected_op)
129 |
130 | def test_terminate_specific_zone(self):
131 | """should terminate only when the given zone has more than wanted items."""
132 | self.task.build(self.world)
133 |
134 | state = DummyState(zones_inventories=np.array([[10, 10, 10], [10, 10, 0]]))
135 | check.is_false(self.task.is_terminal(state))
136 | state = DummyState(zones_inventories=np.array([[0, 0, 0], [0, 0, 2]]))
137 | check.is_true(self.task.is_terminal(state))
138 |
139 | def test_terminate_any_zone(self):
140 | """should terminate when any zone has more than wanted items."""
141 | task = PlaceItemTask(Stack(Item("wood_house"), 2), reward=5)
142 | task.build(self.world)
143 |
144 | state = DummyState(zones_inventories=np.array([[10, 10, 0], [10, 10, 0]]))
145 | check.is_false(task.is_terminal(state))
146 | state = DummyState(zones_inventories=np.array([[0, 0, 0], [0, 0, 2]]))
147 | check.is_true(task.is_terminal(state))
148 | state = DummyState(zones_inventories=np.array([[0, 0, 2], [0, 0, 0]]))
149 | check.is_true(task.is_terminal(state))
150 |
151 | def test_reward(self):
152 | """should reward only the first time the task terminates."""
153 | self.task.build(self.world)
154 |
155 | state = DummyState(zones_inventories=np.array([[10, 10, 10], [10, 10, 0]]))
156 | check.equal(self.task.reward(state), 0)
157 | state = DummyState(zones_inventories=np.array([[0, 0, 0], [0, 0, 2]]))
158 | check.equal(self.task.reward(state), 5)
159 | self.task.terminated = True
160 | check.equal(self.task.reward(state), 0)
161 |
--------------------------------------------------------------------------------
/src/hcraft/examples/minicraft/unlockpickupblocked.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from hcraft.elements import Item, Zone
4 | from hcraft.task import GetItemTask
5 | from hcraft.transformation import Transformation, Use, Yield, PLAYER, CURRENT_ZONE
6 |
7 | from hcraft.examples.minicraft.minicraft import MiniCraftEnv
8 |
9 | MINICRAFT_NAME = "BlockedUnlockPickup"
10 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME, for_module_header=True)
11 |
12 |
13 | class MiniHCraftBlockedUnlockPickup(MiniCraftEnv):
14 | MINICRAFT_NAME = MINICRAFT_NAME
15 | __doc__ = MiniCraftEnv.description(MINICRAFT_NAME)
16 |
17 | START = Zone("start_room")
18 | """Start room."""
19 | BOX_ROOM = Zone("box_room")
20 | """Room with a box inside."""
21 |
22 | KEY = Item("key")
23 | """Key used to unlock the door."""
24 | BOX = Item("box")
25 | """Box to pickup."""
26 | BALL = Item("ball")
27 | """Ball blocking the door."""
28 | WEIGHT = Item("weight")
29 | """Weight of carried items, can only be less than 1."""
30 |
31 | OPEN_DOOR = Item("open_door")
32 | """Open door between the two rooms."""
33 | LOCKED_DOOR = Item("locked_door")
34 | """Locked door between the two rooms, can be unlocked with a key."""
35 | BLOCKED_DOOR = Item("blocked_door")
36 | """Open but blocked door between the two rooms."""
37 | BLOCKED_LOCKED_DOOR = Item("blocked_locked_door")
38 | """Locked and blocked door between the two rooms."""
39 |
40 | def __init__(self, **kwargs) -> None:
41 | self.task = GetItemTask(self.BOX)
42 | super().__init__(
43 | self.MINICRAFT_NAME,
44 | purpose=self.task,
45 | start_zone=self.START,
46 | **kwargs,
47 | )
48 |
49 | def build_transformations(self) -> List[Transformation]:
50 | transformations = []
51 |
52 | zones = (self.START, self.BOX_ROOM)
53 | items_in_zone = [(self.KEY, self.START), (self.BOX, self.BOX_ROOM)]
54 |
55 | for item, zone in items_in_zone:
56 | inventory_changes = [
57 | Yield(CURRENT_ZONE, item, create=1),
58 | # Prevent searching if already found
59 | Yield(PLAYER, item, create=0, max=0),
60 | ]
61 | # Prevent searching if item was placed elsewhere
62 | inventory_changes += [Yield(zone, item, create=0, max=0) for zone in zones]
63 | search_for_item = Transformation(
64 | f"search_for_{item.name}",
65 | inventory_changes=inventory_changes,
66 | zone=zone,
67 | )
68 | transformations.append(search_for_item)
69 |
70 | for item in (self.KEY, self.BOX, self.BALL):
71 | pickup = Transformation(
72 | f"pickup_{item.name}",
73 | inventory_changes=[
74 | Use(CURRENT_ZONE, item, consume=1),
75 | Yield(PLAYER, item, create=1),
76 | # WEIGHT prevents carrying more than one item
77 | Yield(PLAYER, self.WEIGHT, create=1, max=0),
78 | ],
79 | )
80 | put_down = Transformation(
81 | f"put_down_{item.name}",
82 | inventory_changes=[
83 | Use(PLAYER, item, consume=1),
84 | Yield(CURRENT_ZONE, item, create=1),
85 | # WEIGHT prevents carrying more than one item
86 | Use(PLAYER, self.WEIGHT, consume=1),
87 | ],
88 | )
89 | transformations += [pickup, put_down]
90 |
91 | search_for_door = Transformation(
92 | "search_for_door",
93 | inventory_changes=[
94 | Yield(self.START, self.BLOCKED_LOCKED_DOOR, create=1, max=0),
95 | Yield(self.START, self.BLOCKED_DOOR, create=0, max=0),
96 | Yield(self.START, self.LOCKED_DOOR, create=0, max=0),
97 | Yield(self.START, self.OPEN_DOOR, create=0, max=0),
98 | ],
99 | zone=self.START,
100 | )
101 | transformations.append(search_for_door)
102 |
103 | unblock_locked_door = Transformation(
104 | "unblock_locked_door",
105 | inventory_changes=[
106 | Yield(PLAYER, self.BALL, create=1),
107 | Yield(PLAYER, self.WEIGHT, create=1, max=0),
108 | Use(self.START, self.BLOCKED_LOCKED_DOOR, consume=1),
109 | Yield(self.START, self.LOCKED_DOOR, create=1),
110 | ],
111 | )
112 | transformations.append(unblock_locked_door)
113 |
114 | block_locked_door = Transformation(
115 | "block_locked_door",
116 | inventory_changes=[
117 | Use(PLAYER, self.BALL, consume=1),
118 | Use(PLAYER, self.WEIGHT, consume=1),
119 | Use(self.START, self.LOCKED_DOOR, consume=1),
120 | Yield(self.START, self.BLOCKED_LOCKED_DOOR, create=1),
121 | ],
122 | )
123 | transformations.append(block_locked_door)
124 |
125 | unlock_door = Transformation(
126 | "unlock_door",
127 | inventory_changes=[
128 | Use(PLAYER, self.KEY),
129 | Use(self.START, self.LOCKED_DOOR, consume=1),
130 | Yield(self.START, self.OPEN_DOOR, create=1),
131 | ],
132 | )
133 | transformations.append(unlock_door)
134 |
135 | block_door = Transformation(
136 | "block_door",
137 | inventory_changes=[
138 | Use(PLAYER, self.BALL, consume=1),
139 | Use(PLAYER, self.WEIGHT, consume=1),
140 | Use(self.START, self.OPEN_DOOR, consume=1),
141 | Yield(self.START, self.BLOCKED_DOOR, create=1),
142 | ],
143 | )
144 | transformations.append(block_door)
145 |
146 | unblock_door = Transformation(
147 | "unblock_door",
148 | inventory_changes=[
149 | Yield(PLAYER, self.BALL, create=1),
150 | Yield(PLAYER, self.WEIGHT, create=1, max=0),
151 | Use(self.START, self.BLOCKED_DOOR, consume=1),
152 | Yield(self.START, self.OPEN_DOOR, create=1),
153 | ],
154 | )
155 | transformations.append(unblock_door)
156 |
157 | move_to_box_room = Transformation(
158 | "move_to_box_room",
159 | destination=self.BOX_ROOM,
160 | inventory_changes=[Use(CURRENT_ZONE, self.OPEN_DOOR)],
161 | zone=self.START,
162 | )
163 | transformations.append(move_to_box_room)
164 |
165 | move_to_start_room = Transformation(
166 | "move_to_start_room",
167 | destination=self.START,
168 | zone=self.BOX_ROOM,
169 | )
170 | transformations.append(move_to_start_room)
171 |
172 | return transformations
173 |
--------------------------------------------------------------------------------