├── 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 | ![Unlock requirements graph](../../docs/images/requirements_graphs/MiniHCraftUnlock.png) 31 | 32 | 33 | And here is much more complex graph of the 'MineHcraft' environment shown previously: 34 | ![Minehcraft requirements graph](../../docs/images/requirements_graphs/MineHcraft.png) 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 | --------------------------------------------------------------------------------