From 15eb2dde82fa88e2e6cb0e83f0b039c0ee9f0d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 16 Jan 2024 17:08:19 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=A7=20Update=20pre-commit=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 453d4934..15b0d95b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,23 +5,14 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.254' + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.13 hooks: - id: ruff - - repo: https://github.com/psf/black - rev: 22.10.0 - hooks: - - id: black - - repo: local - hooks: - - id: pytest-fast-check - name: pytest-fast-check - entry: ./venv/Scripts/python.exe -m pytest -m "not slow" - stages: ["commit"] - language: system - pass_filenames: false - always_run: true + types_or: [ python, pyi, jupyter ] + args: [ --fix ] + - id: ruff-format + types_or: [ python, pyi, jupyter ] - repo: local hooks: - id: pytest-check From bbb5c3fc08544503b49053cf22606b343b8eba95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 16 Jan 2024 17:27:11 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8E=A8=20Add=20state.as=5Fdict=20for?= =?UTF-8?q?=20easier=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hcraft/state.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/hcraft/state.py b/src/hcraft/state.py index 825bb345..9a63c2e0 100644 --- a/src/hcraft/state.py +++ b/src/hcraft/state.py @@ -1,7 +1,9 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional import numpy as np +from hcraft.transformation import InventoryOwner + if TYPE_CHECKING: from hcraft.world import World from hcraft.elements import Zone, Item @@ -105,6 +107,22 @@ def current_zone(self) -> Optional["Zone"]: def _current_zone_slot(self) -> int: return self.position.nonzero()[0] + @property + def player_inventory_dict(self) -> Dict["Item", int]: + """Current inventory of the player.""" + return self._inv_as_dict(self.player_inventory, self.world.items) + + @property + def zones_inventories_dict(self) -> Dict["Zone", Dict["Item", int]]: + """Current inventories of the current zone and each zone containing item.""" + zones_invs = {} + for zone_slot, zone_inv in enumerate(self.zones_inventories): + zone = self.world.zones[zone_slot] + zone_inv = self._inv_as_dict(zone_inv, self.world.zones_items) + if zone_slot == self._current_zone_slot or zone_inv: + zones_invs[zone] = zone_inv + return zones_invs + def apply(self, action: int) -> bool: """Apply the given action to update the state. @@ -166,3 +184,19 @@ def _update_discoveries(self, action: Optional[int] = None) -> None: self.discovered_zones = np.bitwise_or(self.discovered_zones, self.position > 0) if action is not None: self.discovered_transformations[action] = 1 + + @staticmethod + def _inv_as_dict(inventory_array: np.ndarray, obj_registry: list): + return { + obj_registry[index]: value + for index, value in enumerate(inventory_array) + if value > 0 + } + + def as_dict(self) -> dict: + state_dict = { + "pos": self.current_zone, + InventoryOwner.PLAYER.value: self.player_inventory_dict, + } + state_dict.update(self.zones_inventories_dict) + return state_dict From 7bce83de00d885b82de0d8c73936b39b68a349d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 16 Jan 2024 20:15:42 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Ensure=20no=20env=20is?= =?UTF-8?q?=20built=20from=20module=20initialisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hcraft/examples/minecraft/__init__.py | 12 +++--- src/hcraft/examples/minecraft/env.py | 30 +++++++++++++-- src/hcraft/examples/minecraft/items.py | 37 ++++++++++++++++++- .../examples/minecraft/transformations.py | 12 +++--- src/hcraft/examples/minecraft/zones.py | 2 + src/hcraft/examples/minicraft/__init__.py | 3 +- src/hcraft/purpose.py | 19 ++++++---- src/hcraft/solving_behaviors.py | 1 - tests/examples/minecraft/test_behaviors.py | 28 +++++++------- tests/examples/minecraft/test_gym_make.py | 33 +++++++++++++++-- 10 files changed, 134 insertions(+), 43 deletions(-) diff --git a/src/hcraft/examples/minecraft/__init__.py b/src/hcraft/examples/minecraft/__init__.py index 1559c1a3..63e128a2 100644 --- a/src/hcraft/examples/minecraft/__init__.py +++ b/src/hcraft/examples/minecraft/__init__.py @@ -9,18 +9,20 @@ from typing import Optional import hcraft.examples.minecraft.items as items -from hcraft.examples.minecraft.env import MineHcraftEnv -from hcraft.purpose import Purpose, RewardShaping, platinium_purpose +from hcraft.examples.minecraft.env import ALL_ITEMS, MineHcraftEnv + +from hcraft.purpose import Purpose, RewardShaping from hcraft.task import GetItemTask MINEHCRAFT_GYM_ENVS = [] +__all__ = ["MineHcraftEnv"] + # gym is an optional dependency try: import gym ENV_PATH = "hcraft.examples.minecraft.env:MineHcraftEnv" - MC_WORLD = MineHcraftEnv().world # Simple MineHcraft with no reward, only penalty on illegal actions gym.register( @@ -34,7 +36,7 @@ gym.register( id="MineHcraft-v1", entry_point=ENV_PATH, - kwargs={"purpose": platinium_purpose(MC_WORLD)}, + kwargs={"purpose": "all"}, ) MINEHCRAFT_GYM_ENVS.append("MineHcraft-v1") @@ -71,7 +73,7 @@ def _register_minehcraft_single_item( items.ENDER_DRAGON_HEAD: "Dragon", } - for item in MC_WORLD.items: + for item in ALL_ITEMS: cap_item_name = "".join([part.capitalize() for part in item.name.split("_")]) item_id = replacement_names.get(item, cap_item_name) _register_minehcraft_single_item(item, name=item_id) diff --git a/src/hcraft/examples/minecraft/env.py b/src/hcraft/examples/minecraft/env.py index c5c3fae7..a4720714 100644 --- a/src/hcraft/examples/minecraft/env.py +++ b/src/hcraft/examples/minecraft/env.py @@ -10,13 +10,26 @@ from hcraft.elements import Stack from hcraft.env import HcraftEnv -from hcraft.examples.minecraft.items import CLOSE_ENDER_PORTAL, OPEN_NETHER_PORTAL +from hcraft.examples.minecraft.items import ( + CLOSE_ENDER_PORTAL, + CRAFTABLE_ITEMS, + MC_FINDABLE_ITEMS, + OPEN_NETHER_PORTAL, + PLACABLE_ITEMS, +) +from hcraft.examples.minecraft.tools import MC_TOOLS from hcraft.examples.minecraft.transformations import ( build_minehcraft_transformations, ) -from hcraft.examples.minecraft.zones import FOREST, NETHER, STRONGHOLD +from hcraft.examples.minecraft.zones import FOREST, MC_ZONES, NETHER, STRONGHOLD +from hcraft.purpose import platinium_purpose from hcraft.world import world_from_transformations +ALL_ITEMS = set( + MC_TOOLS + CRAFTABLE_ITEMS + [mcitem.item for mcitem in MC_FINDABLE_ITEMS] +) +"""Set of all items""" + class MineHcraftEnv(HcraftEnv): @@ -31,6 +44,9 @@ def __init__(self, **kwargs): resources_path = os.path.join(mc_dir, "resources") mc_transformations = build_minehcraft_transformations() start_zone = kwargs.pop("start_zone", FOREST) + purpose = kwargs.pop("purpose", None) + if purpose == "all": + purpose = get_platinum_purpose() mc_world = world_from_transformations( mc_transformations, start_zone=start_zone, @@ -40,5 +56,13 @@ def __init__(self, **kwargs): }, ) mc_world.resources_path = resources_path - super().__init__(world=mc_world, name="MineHcraft", **kwargs) + super().__init__(world=mc_world, name="MineHcraft", purpose=purpose, **kwargs) self.metadata["video.frames_per_second"] = kwargs.pop("fps", 10) + + +def get_platinum_purpose(): + return platinium_purpose( + items=list(ALL_ITEMS), + zones=MC_ZONES, + zones_items=PLACABLE_ITEMS, + ) diff --git a/src/hcraft/examples/minecraft/items.py b/src/hcraft/examples/minecraft/items.py index c2eb7ca2..0a2a4292 100644 --- a/src/hcraft/examples/minecraft/items.py +++ b/src/hcraft/examples/minecraft/items.py @@ -43,7 +43,7 @@ """CRAFTING_TABLE""" FURNACE = Item("furnace") -"""CRAFTING_TABLE""" +"""FURNACE""" STICK = Item("stick") """STICK""" @@ -64,6 +64,25 @@ """WOOD_PLANK""" +CRAFTABLE_ITEMS = [ + IRON_INGOT, + GOLD_INGOT, + PAPER, + BOOK, + CLOCK, + ENCHANTING_TABLE, + CRAFTING_TABLE, + FURNACE, + STICK, + BLAZE_POWDER, + ENDER_EYE, + FLINT, + FLINT_AND_STEEL, + WOOD_PLANK, +] +"""Items that can be obtained with crafting.""" + + @dataclass class McItem: """Minecraft item with its specific properties.""" @@ -224,7 +243,7 @@ class McItem: ) """ENDER_DRAGON_HEAD""" -MC_ITEMS = [ +MC_FINDABLE_ITEMS = [ MC_DIRT, MC_WOOD, MC_GRAVEL, @@ -243,6 +262,7 @@ class McItem: MC_ENDER_PEARL, MC_ENDER_DRAGON_HEAD, ] +"""McItems that can be gathered with or without tools.""" #: Buildings CLOSE_NETHER_PORTAL = Item("close_nether_portal") @@ -256,3 +276,16 @@ class McItem: OPEN_ENDER_PORTAL = Item("open_ender_portal") """OPEN_ENDER_PORTAL""" +BUIDINGS = [ + CLOSE_NETHER_PORTAL, + OPEN_NETHER_PORTAL, + CLOSE_ENDER_PORTAL, + OPEN_ENDER_PORTAL, +] + +PLACABLE_ITEMS = [ + CRAFTING_TABLE, + FURNACE, + ENCHANTING_TABLE, +] + BUIDINGS +"""Items that can be placed.""" diff --git a/src/hcraft/examples/minecraft/transformations.py b/src/hcraft/examples/minecraft/transformations.py index f85b6ac8..b9f51589 100644 --- a/src/hcraft/examples/minecraft/transformations.py +++ b/src/hcraft/examples/minecraft/transformations.py @@ -25,11 +25,11 @@ def build_minehcraft_transformations() -> List[Transformation]: transformations = [] - transformations += _move_to_zones() - transformations += _zones_search() transformations += _building() transformations += _recipes() transformations += _tools_recipes() + transformations += _zones_search() + transformations += _move_to_zones() return transformations @@ -49,7 +49,7 @@ def _move_to_zones() -> List[Transformation]: destination=zones.SWAMP, zones=[zone for zone in zones.OVERWORLD if zone != zones.SWAMP], ), - #: Move to zones.MEADOW + #: Move to MEADOW Transformation( name_prefix + zones.MEADOW.name, destination=zones.MEADOW, @@ -112,8 +112,8 @@ def _move_to_zones() -> List[Transformation]: Transformation( name_prefix + zones.STRONGHOLD.name, destination=zones.STRONGHOLD, - zones=zones.OVERWORLD, - inventory_changes=[Use(CURRENT_ZONE, items.OPEN_NETHER_PORTAL, consume=2)], + zones=[zone for zone in zones.OVERWORLD if zone != zones.STRONGHOLD], + inventory_changes=[Use(PLAYER, items.ENDER_EYE)], ), #: Move to zones.END Transformation( @@ -139,7 +139,7 @@ def _zones_search() -> List[Transformation]: name_prefix = "search-for-" search_item = [] - for mc_item in items.MC_ITEMS: + for mc_item in items.MC_FINDABLE_ITEMS: item = mc_item.item if mc_item.required_tool_types is None: diff --git a/src/hcraft/examples/minecraft/zones.py b/src/hcraft/examples/minecraft/zones.py index 7e54f5ec..b78abcd3 100644 --- a/src/hcraft/examples/minecraft/zones.py +++ b/src/hcraft/examples/minecraft/zones.py @@ -17,3 +17,5 @@ STRONGHOLD = Zone("stronghold") #: STRONGHOLD OVERWORLD = [FOREST, SWAMP, MEADOW, UNDERGROUND, BEDROCK, STRONGHOLD] + +MC_ZONES = OVERWORLD + [NETHER, END] diff --git a/src/hcraft/examples/minicraft/__init__.py b/src/hcraft/examples/minicraft/__init__.py index 87e80448..e78ed707 100644 --- a/src/hcraft/examples/minicraft/__init__.py +++ b/src/hcraft/examples/minicraft/__init__.py @@ -47,8 +47,7 @@ ENV_PATH = "hcraft.examples.minicraft" - for env_class in MINICRAFT_ENVS: - env_name = env_class().name + for env_name, env_class in MINICRAFT_NAME_TO_ENV.items(): submodule = Path(inspect.getfile(env_class)).name.split(".")[0] env_path = f"{ENV_PATH}.{submodule}:{env_class.__name__}" gym_name = f"{env_name}-v1" diff --git a/src/hcraft/purpose.py b/src/hcraft/purpose.py index 61e01429..7fec566f 100644 --- a/src/hcraft/purpose.py +++ b/src/hcraft/purpose.py @@ -103,9 +103,10 @@ from hcraft.requirements import RequirementNode, req_node_name from hcraft.task import GetItemTask, GoToZoneTask, PlaceItemTask, Task +from hcraft.elements import Item, Zone + if TYPE_CHECKING: - from hcraft.elements import Item, Zone from hcraft.env import HcraftEnv, HcraftState from hcraft.world import World @@ -353,16 +354,18 @@ def _tasks_str(self, tasks: List[Task]) -> str: def platinium_purpose( - world: "World", + items: List[Item], + zones: List[Zone], + zones_items: List[Item], success_reward: float = 10.0, timestep_reward: float = -0.1, ): purpose = Purpose(timestep_reward=timestep_reward) - for item in world.items: + for item in items: purpose.add_task(GetItemTask(item, reward=success_reward)) - for zone in world.zones: + for zone in zones: purpose.add_task(GoToZoneTask(zone, reward=success_reward)) - for item in world.zones_items: + for item in zones_items: purpose.add_task(PlaceItemTask(item, reward=success_reward)) return purpose @@ -480,9 +483,9 @@ def _inputs_subtasks(task: Task, world: "World", shaping_reward: float) -> List[ def _build_reward_shaping_subtasks( - items: Optional[Union[List["Item"], Set["Item"]]] = None, - zones: Optional[Union[List["Zone"], Set["Zone"]]] = None, - zone_items: Optional[Union[List["Item"], Set["Item"]]] = None, + items: Optional[Union[List[Item], Set[Item]]] = None, + zones: Optional[Union[List[Zone], Set[Zone]]] = None, + zone_items: Optional[Union[List[Item], Set[Item]]] = None, shaping_reward: float = 1.0, ) -> List[Task]: subtasks = [] diff --git a/src/hcraft/solving_behaviors.py b/src/hcraft/solving_behaviors.py index 163d1521..a97d9da0 100644 --- a/src/hcraft/solving_behaviors.py +++ b/src/hcraft/solving_behaviors.py @@ -64,7 +64,6 @@ def build_all_solving_behaviors(world: "World") -> Dict[str, "Behavior"]: all_behaviors = {} all_behaviors = _reach_zones_behaviors(world, all_behaviors) all_behaviors = _get_item_behaviors(world, all_behaviors) - # all_behaviors = _drop_item_behaviors(world, all_behaviors) all_behaviors = _get_zone_item_behaviors(world, all_behaviors) all_behaviors = _do_transfo_behaviors(world, all_behaviors) return all_behaviors diff --git a/tests/examples/minecraft/test_behaviors.py b/tests/examples/minecraft/test_behaviors.py index 25676908..1bce1835 100644 --- a/tests/examples/minecraft/test_behaviors.py +++ b/tests/examples/minecraft/test_behaviors.py @@ -12,6 +12,7 @@ from hcraft.behaviors.utils import get_items_in_graph, get_zones_items_in_graph from hcraft.examples.minecraft.env import MineHcraftEnv from hcraft.examples.minecraft.items import ( + MC_FINDABLE_ITEMS, OPEN_NETHER_PORTAL, STICK, WOOD_PLANK, @@ -22,7 +23,6 @@ ToolType, ) from hcraft.examples.minecraft.zones import NETHER, UNDERGROUND -from hcraft.purpose import platinium_purpose from hcraft.task import Task, GetItemTask, GoToZoneTask if TYPE_CHECKING: @@ -32,8 +32,7 @@ STONE_PICKAXE = MC_TOOLS_BY_TYPE_AND_MATERIAL[ToolType.PICKAXE][Material.STONE] -KNOWN_TO_FAIL_TASK_HEBG = [ - "Place enchanting_table anywhere", +HARD_TASKS = [ "Place open_ender_portal anywhere", "Go to stronghold", "Go to end", @@ -41,19 +40,24 @@ ] -@pytest.mark.slow +@pytest.mark.xfail(reason="Destination items are not taken into account") def test_solving_behaviors(): """All tasks should be solved by their solving behavior.""" - env = MineHcraftEnv(purpose=platinium_purpose(MineHcraftEnv().world), max_step=500) + env = MineHcraftEnv(purpose="all", max_step=500) done = False observation = env.reset() - solving_behavior = None tasks_left = env.purpose.tasks.copy() - task = None + task = [t for t in tasks_left if t.name == "Place enchanting_table anywhere"][0] + solving_behavior = env.solving_behavior(task) + easy_tasks_left = [task] while not done and not env.purpose.terminated: tasks_left = [t for t in tasks_left if not t.terminated] if task is None: - task = tasks_left[0] + easy_tasks_left = [t for t in tasks_left if t.name not in HARD_TASKS] + if len(easy_tasks_left) > 0: + task = easy_tasks_left[0] + else: + task = tasks_left[0] print(f"Task started: {task} (step={env.current_step})") solving_behavior = env.solving_behavior(task) action = solving_behavior(observation) @@ -63,10 +67,8 @@ def test_solving_behaviors(): task = None if isinstance(task, Task) and not task.terminated: print(f"Last unfinished task: {task}") - if set(t.name for t in tasks_left) == set(KNOWN_TO_FAIL_TASK_HEBG): - pytest.xfail( - f"Harder tasks ({KNOWN_TO_FAIL_TASK_HEBG}) cannot be done for now ..." - ) + if set(t.name for t in tasks_left) == set(HARD_TASKS): + pytest.xfail(f"Harder tasks ({HARD_TASKS}) cannot be done for now ...") check.is_true(env.purpose.terminated, msg=f"tasks not completed: {tasks_left}") @@ -103,7 +105,7 @@ def test_solving_behaviors(): @pytest.mark.slow -@pytest.mark.parametrize("item", [item.name for item in MineHcraftEnv().world.items]) +@pytest.mark.parametrize("item", [item.item.name for item in MC_FINDABLE_ITEMS]) def test_get_all_items_pddl(item: str): """All items should be gettable by planning behavior.""" up = pytest.importorskip("unified_planning") diff --git a/tests/examples/minecraft/test_gym_make.py b/tests/examples/minecraft/test_gym_make.py index 99743e87..d529cfe2 100644 --- a/tests/examples/minecraft/test_gym_make.py +++ b/tests/examples/minecraft/test_gym_make.py @@ -1,8 +1,10 @@ +from typing import List, Type, TypeVar import pytest import pytest_check as check from hcraft.examples.minecraft.env import MineHcraftEnv +from hcraft.task import GetItemTask, GoToZoneTask, PlaceItemTask, Task gym = pytest.importorskip("gym") @@ -42,9 +44,34 @@ def test_enchanting_table_gym_make(): def test_all_items_gym_make(): env: MineHcraftEnv = gym.make("MineHcraft-v1") - check.equal( - len(env.purpose.tasks), - env.world.n_items + env.world.n_zones + env.world.n_zones_items, + + TaskOfType = TypeVar("TaskOfType") + + def _task_names_of_type( + tasks: List[Task], task_type: Type[TaskOfType] + ) -> List[TaskOfType]: + return [task for task in tasks if isinstance(task, task_type)] + + check.assert_equal( + set( + task.item_stack.item.name + for task in _task_names_of_type(env.purpose.tasks, GetItemTask) + ), + set(item.name for item in env.world.items), + ) + check.assert_equal( + set( + task.zone.name + for task in _task_names_of_type(env.purpose.tasks, GoToZoneTask) + ), + set(zone.name for zone in env.world.zones), + ) + check.assert_equal( + set( + task.item_stack.item.name + for task in _task_names_of_type(env.purpose.tasks, PlaceItemTask) + ), + set(item.name for item in env.world.zones_items), ) From 1af3f878f6c5804ecfefede8e06edb00d9e2c8aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 16 Jan 2024 20:26:23 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A7=AA=20ENHSP=20fails=20more=20on=20?= =?UTF-8?q?MineHCraft=20now=20=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/examples/minecraft/test_behaviors.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/examples/minecraft/test_behaviors.py b/tests/examples/minecraft/test_behaviors.py index 1bce1835..1dfda383 100644 --- a/tests/examples/minecraft/test_behaviors.py +++ b/tests/examples/minecraft/test_behaviors.py @@ -10,9 +10,8 @@ from hcraft.elements import Item from hcraft.behaviors.utils import get_items_in_graph, get_zones_items_in_graph -from hcraft.examples.minecraft.env import MineHcraftEnv +from hcraft.examples.minecraft.env import ALL_ITEMS, MineHcraftEnv from hcraft.examples.minecraft.items import ( - MC_FINDABLE_ITEMS, OPEN_NETHER_PORTAL, STICK, WOOD_PLANK, @@ -73,6 +72,11 @@ def test_solving_behaviors(): KNOWN_TO_FAIL_ITEM_ENHSP = [ + "reeds", + "wood_pickaxe", + "wood_axe", + "cobblestone", + "coal", "leather", "book", "flint_and_steel", @@ -105,7 +109,7 @@ def test_solving_behaviors(): @pytest.mark.slow -@pytest.mark.parametrize("item", [item.item.name for item in MC_FINDABLE_ITEMS]) +@pytest.mark.parametrize("item", [item.name for item in ALL_ITEMS]) def test_get_all_items_pddl(item: str): """All items should be gettable by planning behavior.""" up = pytest.importorskip("unified_planning")