1from typing import TYPE_CHECKING, Dict, Optional
  2
  3import numpy as np
  4
  5from hcraft.transformation import InventoryOwner
  6
  7if TYPE_CHECKING:
  8    from hcraft.world import World
  9    from hcraft.elements import Zone, Item
 10
 11
 12class HcraftState:
 13    """State manager of HierarchyCraft environments.
 14
 15    The state of every HierarchyCraft environment is composed of three parts:
 16    * The player's inventory: `state.player_inventory`
 17    * The one-hot encoded player's position: `state.position`
 18    * All zones inventories: `state.zones_inventories`
 19
 20    The mapping of items, zones, and zones items to their respective indexes is done through
 21    the given World. (See `hcraft.world`)
 22
 23    ![hcraft state](../../docs/images/hcraft_state.png)
 24
 25    """
 26
 27    def __init__(self, world: "World") -> None:
 28        """
 29        Args:
 30            world: World to build the state for.
 31        """
 32        self.player_inventory = np.array([], dtype=np.int32)
 33        self.position = np.array([], dtype=np.int32)
 34        self.zones_inventories = np.array([], dtype=np.int32)
 35
 36        self.discovered_items = np.array([], dtype=np.ubyte)
 37        self.discovered_zones = np.array([], dtype=np.ubyte)
 38        self.discovered_zones_items = np.array([], dtype=np.ubyte)
 39        self.discovered_transformations = np.array([], dtype=np.ubyte)
 40
 41        self.world = world
 42        self.reset()
 43
 44    @property
 45    def current_zone_inventory(self) -> np.ndarray:
 46        """Inventory of the zone where the player is."""
 47        if self.position.shape[0] == 0:
 48            return np.array([])  # No Zone
 49        return self.zones_inventories[self._current_zone_slot, :][0]
 50
 51    @property
 52    def observation(self) -> np.ndarray:
 53        """The player's observation is a subset of the state.
 54
 55        Only the inventory of the current zone is shown.
 56
 57        ![hcraft state](../../docs/images/hcraft_observation.png)
 58
 59        """
 60        return np.concatenate(
 61            (
 62                self.player_inventory,
 63                self.position,
 64                self.current_zone_inventory,
 65            )
 66        )
 67
 68    def amount_of(self, item: "Item", owner: Optional["Zone"] = "player") -> int:
 69        """Current amount of the given item owned by owner.
 70
 71        Args:
 72            item: Item to get the amount of.
 73            owner: Owner of the inventory to check. Defaults to player.
 74
 75        Returns:
 76            int: Amount of the item in the owner's inventory.
 77        """
 78
 79        if owner in self.world.zones:
 80            zone_index = self.world.zones.index(owner)
 81            zone_item_index = self.world.zones_items.index(item)
 82            return int(self.zones_inventories[zone_index, zone_item_index])
 83
 84        item_index = self.world.items.index(item)
 85        return int(self.player_inventory[item_index])
 86
 87    def has_discovered(self, zone: "Zone") -> bool:
 88        """Whether the given zone was discovered.
 89
 90        Args:
 91            zone (Zone): Zone to check.
 92
 93        Returns:
 94            bool: True if the zone was discovered.
 95        """
 96        zone_index = self.world.zones.index(zone)
 97        return bool(self.discovered_zones[zone_index])
 98
 99    @property
100    def current_zone(self) -> Optional["Zone"]:
101        """Current position of the player."""
102        if self.world.n_zones == 0:
103            return None
104        return self.world.zones[self._current_zone_slot[0]]
105
106    @property
107    def _current_zone_slot(self) -> int:
108        return self.position.nonzero()[0]
109
110    @property
111    def player_inventory_dict(self) -> Dict["Item", int]:
112        """Current inventory of the player."""
113        return self._inv_as_dict(self.player_inventory, self.world.items)
114
115    @property
116    def zones_inventories_dict(self) -> Dict["Zone", Dict["Item", int]]:
117        """Current inventories of the current zone and each zone containing item."""
118        zones_invs = {}
119        for zone_slot, zone_inv in enumerate(self.zones_inventories):
120            zone = self.world.zones[zone_slot]
121            zone_inv = self._inv_as_dict(zone_inv, self.world.zones_items)
122            if zone_slot == self._current_zone_slot or zone_inv:
123                zones_invs[zone] = zone_inv
124        return zones_invs
125
126    def apply(self, action: int) -> bool:
127        """Apply the given action to update the state.
128
129        Args:
130            action (int): Index of the transformation to apply.
131
132        Returns:
133            bool: True if the transformation was applied succesfuly. False otherwise.
134        """
135        choosen_transformation = self.world.transformations[action]
136        if not choosen_transformation.is_valid(self):
137            return False
138        choosen_transformation.apply(
139            self.player_inventory,
140            self.position,
141            self.zones_inventories,
142        )
143        self._update_discoveries(action)
144        return True
145
146    def reset(self) -> None:
147        """Reset the state to it's initial value."""
148        self.player_inventory = np.zeros(self.world.n_items, dtype=np.int32)
149        for stack in self.world.start_items:
150            item_slot = self.world.items.index(stack.item)
151            self.player_inventory[item_slot] = stack.quantity
152
153        self.position = np.zeros(self.world.n_zones, dtype=np.int32)
154        start_slot = 0  # Start in first Zone by default
155        if self.world.start_zone is not None:
156            start_slot = self.world.slot_from_zone(self.world.start_zone)
157        if self.position.shape[0] > 0:
158            self.position[start_slot] = 1
159
160        self.zones_inventories = np.zeros(
161            (self.world.n_zones, self.world.n_zones_items), dtype=np.int32
162        )
163        for zone, zone_stacks in self.world.start_zones_items.items():
164            zone_slot = self.world.slot_from_zone(zone)
165            for stack in zone_stacks:
166                item_slot = self.world.zones_items.index(stack.item)
167                self.zones_inventories[zone_slot, item_slot] = stack.quantity
168
169        self.discovered_items = np.zeros(self.world.n_items, dtype=np.ubyte)
170        self.discovered_zones_items = np.zeros(self.world.n_zones_items, dtype=np.ubyte)
171        self.discovered_zones = np.zeros(self.world.n_zones, dtype=np.ubyte)
172        self.discovered_transformations = np.zeros(
173            len(self.world.transformations), dtype=np.ubyte
174        )
175        self._update_discoveries()
176
177    def _update_discoveries(self, action: Optional[int] = None) -> None:
178        self.discovered_items = np.bitwise_or(
179            self.discovered_items, self.player_inventory > 0
180        )
181        self.discovered_zones_items = np.bitwise_or(
182            self.discovered_zones_items, self.current_zone_inventory > 0
183        )
184        self.discovered_zones = np.bitwise_or(self.discovered_zones, self.position > 0)
185        if action is not None:
186            self.discovered_transformations[action] = 1
187
188    @staticmethod
189    def _inv_as_dict(inventory_array: np.ndarray, obj_registry: list):
190        return {
191            obj_registry[index]: value
192            for index, value in enumerate(inventory_array)
193            if value > 0
194        }
195
196    def as_dict(self) -> dict:
197        state_dict = {
198            "pos": self.current_zone,
199            InventoryOwner.PLAYER.value: self.player_inventory_dict,
200        }
201        state_dict.update(self.zones_inventories_dict)
202        return state_dict

API Documentation

class HcraftState:
 13class HcraftState:
 14    """State manager of HierarchyCraft environments.
 15
 16    The state of every HierarchyCraft environment is composed of three parts:
 17    * The player's inventory: `state.player_inventory`
 18    * The one-hot encoded player's position: `state.position`
 19    * All zones inventories: `state.zones_inventories`
 20
 21    The mapping of items, zones, and zones items to their respective indexes is done through
 22    the given World. (See `hcraft.world`)
 23
 24    ![hcraft state](../../docs/images/hcraft_state.png)
 25
 26    """
 27
 28    def __init__(self, world: "World") -> None:
 29        """
 30        Args:
 31            world: World to build the state for.
 32        """
 33        self.player_inventory = np.array([], dtype=np.int32)
 34        self.position = np.array([], dtype=np.int32)
 35        self.zones_inventories = np.array([], dtype=np.int32)
 36
 37        self.discovered_items = np.array([], dtype=np.ubyte)
 38        self.discovered_zones = np.array([], dtype=np.ubyte)
 39        self.discovered_zones_items = np.array([], dtype=np.ubyte)
 40        self.discovered_transformations = np.array([], dtype=np.ubyte)
 41
 42        self.world = world
 43        self.reset()
 44
 45    @property
 46    def current_zone_inventory(self) -> np.ndarray:
 47        """Inventory of the zone where the player is."""
 48        if self.position.shape[0] == 0:
 49            return np.array([])  # No Zone
 50        return self.zones_inventories[self._current_zone_slot, :][0]
 51
 52    @property
 53    def observation(self) -> np.ndarray:
 54        """The player's observation is a subset of the state.
 55
 56        Only the inventory of the current zone is shown.
 57
 58        ![hcraft state](../../docs/images/hcraft_observation.png)
 59
 60        """
 61        return np.concatenate(
 62            (
 63                self.player_inventory,
 64                self.position,
 65                self.current_zone_inventory,
 66            )
 67        )
 68
 69    def amount_of(self, item: "Item", owner: Optional["Zone"] = "player") -> int:
 70        """Current amount of the given item owned by owner.
 71
 72        Args:
 73            item: Item to get the amount of.
 74            owner: Owner of the inventory to check. Defaults to player.
 75
 76        Returns:
 77            int: Amount of the item in the owner's inventory.
 78        """
 79
 80        if owner in self.world.zones:
 81            zone_index = self.world.zones.index(owner)
 82            zone_item_index = self.world.zones_items.index(item)
 83            return int(self.zones_inventories[zone_index, zone_item_index])
 84
 85        item_index = self.world.items.index(item)
 86        return int(self.player_inventory[item_index])
 87
 88    def has_discovered(self, zone: "Zone") -> bool:
 89        """Whether the given zone was discovered.
 90
 91        Args:
 92            zone (Zone): Zone to check.
 93
 94        Returns:
 95            bool: True if the zone was discovered.
 96        """
 97        zone_index = self.world.zones.index(zone)
 98        return bool(self.discovered_zones[zone_index])
 99
100    @property
101    def current_zone(self) -> Optional["Zone"]:
102        """Current position of the player."""
103        if self.world.n_zones == 0:
104            return None
105        return self.world.zones[self._current_zone_slot[0]]
106
107    @property
108    def _current_zone_slot(self) -> int:
109        return self.position.nonzero()[0]
110
111    @property
112    def player_inventory_dict(self) -> Dict["Item", int]:
113        """Current inventory of the player."""
114        return self._inv_as_dict(self.player_inventory, self.world.items)
115
116    @property
117    def zones_inventories_dict(self) -> Dict["Zone", Dict["Item", int]]:
118        """Current inventories of the current zone and each zone containing item."""
119        zones_invs = {}
120        for zone_slot, zone_inv in enumerate(self.zones_inventories):
121            zone = self.world.zones[zone_slot]
122            zone_inv = self._inv_as_dict(zone_inv, self.world.zones_items)
123            if zone_slot == self._current_zone_slot or zone_inv:
124                zones_invs[zone] = zone_inv
125        return zones_invs
126
127    def apply(self, action: int) -> bool:
128        """Apply the given action to update the state.
129
130        Args:
131            action (int): Index of the transformation to apply.
132
133        Returns:
134            bool: True if the transformation was applied succesfuly. False otherwise.
135        """
136        choosen_transformation = self.world.transformations[action]
137        if not choosen_transformation.is_valid(self):
138            return False
139        choosen_transformation.apply(
140            self.player_inventory,
141            self.position,
142            self.zones_inventories,
143        )
144        self._update_discoveries(action)
145        return True
146
147    def reset(self) -> None:
148        """Reset the state to it's initial value."""
149        self.player_inventory = np.zeros(self.world.n_items, dtype=np.int32)
150        for stack in self.world.start_items:
151            item_slot = self.world.items.index(stack.item)
152            self.player_inventory[item_slot] = stack.quantity
153
154        self.position = np.zeros(self.world.n_zones, dtype=np.int32)
155        start_slot = 0  # Start in first Zone by default
156        if self.world.start_zone is not None:
157            start_slot = self.world.slot_from_zone(self.world.start_zone)
158        if self.position.shape[0] > 0:
159            self.position[start_slot] = 1
160
161        self.zones_inventories = np.zeros(
162            (self.world.n_zones, self.world.n_zones_items), dtype=np.int32
163        )
164        for zone, zone_stacks in self.world.start_zones_items.items():
165            zone_slot = self.world.slot_from_zone(zone)
166            for stack in zone_stacks:
167                item_slot = self.world.zones_items.index(stack.item)
168                self.zones_inventories[zone_slot, item_slot] = stack.quantity
169
170        self.discovered_items = np.zeros(self.world.n_items, dtype=np.ubyte)
171        self.discovered_zones_items = np.zeros(self.world.n_zones_items, dtype=np.ubyte)
172        self.discovered_zones = np.zeros(self.world.n_zones, dtype=np.ubyte)
173        self.discovered_transformations = np.zeros(
174            len(self.world.transformations), dtype=np.ubyte
175        )
176        self._update_discoveries()
177
178    def _update_discoveries(self, action: Optional[int] = None) -> None:
179        self.discovered_items = np.bitwise_or(
180            self.discovered_items, self.player_inventory > 0
181        )
182        self.discovered_zones_items = np.bitwise_or(
183            self.discovered_zones_items, self.current_zone_inventory > 0
184        )
185        self.discovered_zones = np.bitwise_or(self.discovered_zones, self.position > 0)
186        if action is not None:
187            self.discovered_transformations[action] = 1
188
189    @staticmethod
190    def _inv_as_dict(inventory_array: np.ndarray, obj_registry: list):
191        return {
192            obj_registry[index]: value
193            for index, value in enumerate(inventory_array)
194            if value > 0
195        }
196
197    def as_dict(self) -> dict:
198        state_dict = {
199            "pos": self.current_zone,
200            InventoryOwner.PLAYER.value: self.player_inventory_dict,
201        }
202        state_dict.update(self.zones_inventories_dict)
203        return state_dict

State manager of HierarchyCraft environments.

The state of every HierarchyCraft environment is composed of three parts:

  • The player's inventory: state.player_inventory
  • The one-hot encoded player's position: state.position
  • All zones inventories: state.zones_inventories

The mapping of items, zones, and zones items to their respective indexes is done through the given World. (See hcraft.world)

hcraft state

HcraftState(world: hcraft.world.World)
28    def __init__(self, world: "World") -> None:
29        """
30        Args:
31            world: World to build the state for.
32        """
33        self.player_inventory = np.array([], dtype=np.int32)
34        self.position = np.array([], dtype=np.int32)
35        self.zones_inventories = np.array([], dtype=np.int32)
36
37        self.discovered_items = np.array([], dtype=np.ubyte)
38        self.discovered_zones = np.array([], dtype=np.ubyte)
39        self.discovered_zones_items = np.array([], dtype=np.ubyte)
40        self.discovered_transformations = np.array([], dtype=np.ubyte)
41
42        self.world = world
43        self.reset()
Arguments:
  • world: World to build the state for.
player_inventory
position
zones_inventories
discovered_items
discovered_zones
discovered_zones_items
discovered_transformations
world
current_zone_inventory: numpy.ndarray
45    @property
46    def current_zone_inventory(self) -> np.ndarray:
47        """Inventory of the zone where the player is."""
48        if self.position.shape[0] == 0:
49            return np.array([])  # No Zone
50        return self.zones_inventories[self._current_zone_slot, :][0]

Inventory of the zone where the player is.

observation: numpy.ndarray
52    @property
53    def observation(self) -> np.ndarray:
54        """The player's observation is a subset of the state.
55
56        Only the inventory of the current zone is shown.
57
58        ![hcraft state](../../docs/images/hcraft_observation.png)
59
60        """
61        return np.concatenate(
62            (
63                self.player_inventory,
64                self.position,
65                self.current_zone_inventory,
66            )
67        )

The player's observation is a subset of the state.

Only the inventory of the current zone is shown.

hcraft state

def amount_of( self, item: hcraft.Item, owner: Optional[hcraft.Zone] = 'player') -> int:
69    def amount_of(self, item: "Item", owner: Optional["Zone"] = "player") -> int:
70        """Current amount of the given item owned by owner.
71
72        Args:
73            item: Item to get the amount of.
74            owner: Owner of the inventory to check. Defaults to player.
75
76        Returns:
77            int: Amount of the item in the owner's inventory.
78        """
79
80        if owner in self.world.zones:
81            zone_index = self.world.zones.index(owner)
82            zone_item_index = self.world.zones_items.index(item)
83            return int(self.zones_inventories[zone_index, zone_item_index])
84
85        item_index = self.world.items.index(item)
86        return int(self.player_inventory[item_index])

Current amount of the given item owned by owner.

Arguments:
  • item: Item to get the amount of.
  • owner: Owner of the inventory to check. Defaults to player.
Returns:

int: Amount of the item in the owner's inventory.

def has_discovered(self, zone: hcraft.Zone) -> bool:
88    def has_discovered(self, zone: "Zone") -> bool:
89        """Whether the given zone was discovered.
90
91        Args:
92            zone (Zone): Zone to check.
93
94        Returns:
95            bool: True if the zone was discovered.
96        """
97        zone_index = self.world.zones.index(zone)
98        return bool(self.discovered_zones[zone_index])

Whether the given zone was discovered.

Arguments:
  • zone (Zone): Zone to check.
Returns:

bool: True if the zone was discovered.

current_zone: Optional[hcraft.Zone]
100    @property
101    def current_zone(self) -> Optional["Zone"]:
102        """Current position of the player."""
103        if self.world.n_zones == 0:
104            return None
105        return self.world.zones[self._current_zone_slot[0]]

Current position of the player.

player_inventory_dict: Dict[hcraft.Item, int]
111    @property
112    def player_inventory_dict(self) -> Dict["Item", int]:
113        """Current inventory of the player."""
114        return self._inv_as_dict(self.player_inventory, self.world.items)

Current inventory of the player.

zones_inventories_dict: Dict[hcraft.Zone, Dict[hcraft.Item, int]]
116    @property
117    def zones_inventories_dict(self) -> Dict["Zone", Dict["Item", int]]:
118        """Current inventories of the current zone and each zone containing item."""
119        zones_invs = {}
120        for zone_slot, zone_inv in enumerate(self.zones_inventories):
121            zone = self.world.zones[zone_slot]
122            zone_inv = self._inv_as_dict(zone_inv, self.world.zones_items)
123            if zone_slot == self._current_zone_slot or zone_inv:
124                zones_invs[zone] = zone_inv
125        return zones_invs

Current inventories of the current zone and each zone containing item.

def apply(self, action: int) -> bool:
127    def apply(self, action: int) -> bool:
128        """Apply the given action to update the state.
129
130        Args:
131            action (int): Index of the transformation to apply.
132
133        Returns:
134            bool: True if the transformation was applied succesfuly. False otherwise.
135        """
136        choosen_transformation = self.world.transformations[action]
137        if not choosen_transformation.is_valid(self):
138            return False
139        choosen_transformation.apply(
140            self.player_inventory,
141            self.position,
142            self.zones_inventories,
143        )
144        self._update_discoveries(action)
145        return True

Apply the given action to update the state.

Arguments:
  • action (int): Index of the transformation to apply.
Returns:

bool: True if the transformation was applied succesfuly. False otherwise.

def reset(self) -> None:
147    def reset(self) -> None:
148        """Reset the state to it's initial value."""
149        self.player_inventory = np.zeros(self.world.n_items, dtype=np.int32)
150        for stack in self.world.start_items:
151            item_slot = self.world.items.index(stack.item)
152            self.player_inventory[item_slot] = stack.quantity
153
154        self.position = np.zeros(self.world.n_zones, dtype=np.int32)
155        start_slot = 0  # Start in first Zone by default
156        if self.world.start_zone is not None:
157            start_slot = self.world.slot_from_zone(self.world.start_zone)
158        if self.position.shape[0] > 0:
159            self.position[start_slot] = 1
160
161        self.zones_inventories = np.zeros(
162            (self.world.n_zones, self.world.n_zones_items), dtype=np.int32
163        )
164        for zone, zone_stacks in self.world.start_zones_items.items():
165            zone_slot = self.world.slot_from_zone(zone)
166            for stack in zone_stacks:
167                item_slot = self.world.zones_items.index(stack.item)
168                self.zones_inventories[zone_slot, item_slot] = stack.quantity
169
170        self.discovered_items = np.zeros(self.world.n_items, dtype=np.ubyte)
171        self.discovered_zones_items = np.zeros(self.world.n_zones_items, dtype=np.ubyte)
172        self.discovered_zones = np.zeros(self.world.n_zones, dtype=np.ubyte)
173        self.discovered_transformations = np.zeros(
174            len(self.world.transformations), dtype=np.ubyte
175        )
176        self._update_discoveries()

Reset the state to it's initial value.

def as_dict(self) -> dict:
197    def as_dict(self) -> dict:
198        state_dict = {
199            "pos": self.current_zone,
200            InventoryOwner.PLAYER.value: self.player_inventory_dict,
201        }
202        state_dict.update(self.zones_inventories_dict)
203        return state_dict