plancraft 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
File without changes
@@ -0,0 +1,218 @@
1
+ from typing import Union
2
+
3
+ from pydantic import BaseModel, field_validator, model_validator
4
+
5
+
6
+ def convert_to_slot_index(slot: str) -> int:
7
+ slot = slot.strip()
8
+ grid_map = {
9
+ "[0]": 0,
10
+ "[A1]": 1,
11
+ "[A2]": 2,
12
+ "[A3]": 3,
13
+ "[B1]": 4,
14
+ "[B2]": 5,
15
+ "[B3]": 6,
16
+ "[C1]": 7,
17
+ "[C2]": 8,
18
+ "[C3]": 9,
19
+ }
20
+ if slot in grid_map:
21
+ return grid_map[slot]
22
+ else:
23
+ return int(slot[2:-1]) + 9
24
+
25
+
26
+ def convert_from_slot_index(slot_index: int) -> str:
27
+ grid_map = {
28
+ 0: "[0]",
29
+ 1: "[A1]",
30
+ 2: "[A2]",
31
+ 3: "[A3]",
32
+ 4: "[B1]",
33
+ 5: "[B2]",
34
+ 6: "[B3]",
35
+ 7: "[C1]",
36
+ 8: "[C2]",
37
+ 9: "[C3]",
38
+ }
39
+ if slot_index < 10:
40
+ return grid_map[slot_index]
41
+ else:
42
+ return f"[I{slot_index-9}]"
43
+
44
+
45
+ class SymbolicMoveAction(BaseModel):
46
+ """ "Moves an item from one slot to another"""
47
+
48
+ slot_from: int
49
+ slot_to: int
50
+ quantity: int
51
+ action_type: str = "move"
52
+
53
+ @field_validator("action_type", mode="before")
54
+ def fix_action_type(cls, value) -> str:
55
+ return "move"
56
+
57
+ @field_validator("slot_from", "slot_to", mode="before")
58
+ def transform_str_to_int(cls, value) -> int:
59
+ # if value is a string like [A1] or [I1], convert it to an integer
60
+ if isinstance(value, str):
61
+ try:
62
+ return convert_to_slot_index(value)
63
+ except ValueError:
64
+ raise AttributeError(
65
+ "slot_from and slot_to must be [0] or [A1] to [C3] or [I1] to [I36]"
66
+ )
67
+ return value
68
+
69
+ @field_validator("quantity", mode="before")
70
+ def transform_quantity(cls, value) -> int:
71
+ if isinstance(value, str):
72
+ try:
73
+ return int(value)
74
+ except ValueError:
75
+ raise AttributeError("quantity must be an integer")
76
+ return value
77
+
78
+ @model_validator(mode="after")
79
+ def validate(self):
80
+ if self.slot_from == self.slot_to:
81
+ raise AttributeError("slot_from and slot_to must be different")
82
+ if self.slot_from < 0 or self.slot_from > 45:
83
+ raise AttributeError("slot_from must be between 0 and 45")
84
+ if self.slot_to < 1 or self.slot_to > 45:
85
+ raise AttributeError("slot_to must be between 1 and 45")
86
+ if self.quantity < 1 or self.quantity > 64:
87
+ raise AttributeError("quantity must be between 1 and 64")
88
+
89
+ def to_action_dict(self) -> dict:
90
+ return {
91
+ "inventory_command": [self.slot_from, self.slot_to, self.quantity],
92
+ }
93
+
94
+
95
+ class SymbolicSmeltAction(BaseModel):
96
+ """Smelts an item and moves the result into a new slot"""
97
+
98
+ slot_from: int
99
+ slot_to: int
100
+ quantity: int
101
+ action_type: str = "smelt"
102
+
103
+ @field_validator("action_type", mode="before")
104
+ def fix_action_type(cls, value) -> str:
105
+ return "smelt"
106
+
107
+ @field_validator("slot_from", "slot_to", mode="before")
108
+ def transform_str_to_int(cls, value) -> int:
109
+ # if value is a string like [A1] or [I1], convert it to an integer
110
+ if isinstance(value, str):
111
+ try:
112
+ return convert_to_slot_index(value)
113
+ except ValueError:
114
+ raise AttributeError(
115
+ "slot_from and slot_to must be [0] or [A1] to [C3] or [I1] to [I36]"
116
+ )
117
+ return value
118
+
119
+ @field_validator("quantity", mode="before")
120
+ def transform_quantity(cls, value) -> int:
121
+ if isinstance(value, str):
122
+ try:
123
+ return int(value)
124
+ except ValueError:
125
+ raise AttributeError("quantity must be an integer")
126
+ return value
127
+
128
+ @model_validator(mode="after")
129
+ def validate(self):
130
+ if self.slot_from == self.slot_to:
131
+ raise AttributeError("slot_from and slot_to must be different")
132
+ if self.slot_from < 0 or self.slot_from > 45:
133
+ raise AttributeError("slot_from must be between 0 and 45")
134
+ if self.slot_to < 1 or self.slot_to > 45:
135
+ raise AttributeError("slot_to must be between 1 and 45")
136
+ if self.quantity < 1 or self.quantity > 64:
137
+ raise AttributeError("quantity must be between 1 and 64")
138
+
139
+ def to_action_dict(self) -> dict:
140
+ return {
141
+ "smelt": [self.slot_from, self.slot_to, self.quantity],
142
+ }
143
+
144
+
145
+ class ThinkAction(BaseModel):
146
+ """Think about the answer before answering"""
147
+
148
+ thought: str
149
+
150
+ def to_action_dict(self) -> dict:
151
+ return {}
152
+
153
+
154
+ class SearchAction(BaseModel):
155
+ """Searches for a relevant document in the wiki"""
156
+
157
+ search_string: str
158
+
159
+ def to_action_dict(self) -> dict:
160
+ return {
161
+ "search": self.search_string,
162
+ }
163
+
164
+
165
+ class RealActionInteraction(BaseModel):
166
+ mouse_direction_x: float = 0
167
+ mouse_direction_y: float = 0
168
+ right_click: bool = False
169
+ left_click: bool = False
170
+
171
+ @field_validator("mouse_direction_x", "mouse_direction_y")
172
+ def prevent_zero(cls, v):
173
+ if v > 10:
174
+ return 10
175
+ elif v < -10:
176
+ return -10
177
+ return v
178
+
179
+ def to_action_dict(self) -> dict:
180
+ return {
181
+ "camera": [self.mouse_direction_x, self.mouse_direction_y],
182
+ "use": int(self.right_click),
183
+ "attack": int(self.left_click),
184
+ }
185
+
186
+
187
+ class StopAction(BaseModel):
188
+ """
189
+ Action that model can take to stop planning - decide impossible to continue
190
+ Note: also known as the "impossible" action
191
+ """
192
+
193
+ reason: str = ""
194
+
195
+
196
+ class NoOp(SymbolicMoveAction):
197
+ """No operation action - special instance of move"""
198
+
199
+ def __init__(self):
200
+ super().__init__(slot_from=0, slot_to=1, quantity=1)
201
+ self.slot_to = 0
202
+
203
+ def __call__(self, *args, **kwargs):
204
+ return None
205
+
206
+ def __str__(self):
207
+ return "NoOp"
208
+
209
+
210
+ # when symbolic action is true, can either move objects around or smelt
211
+ SymbolicAction = SymbolicMoveAction # | SymbolicSmeltAction
212
+
213
+ # when symbolic action is false, then need to use mouse to move things around, but can use smelt action
214
+ RealAction = RealActionInteraction | SymbolicSmeltAction
215
+
216
+
217
+ class PydanticSymbolicAction(BaseModel):
218
+ root: Union[SymbolicMoveAction, SymbolicSmeltAction]
@@ -0,0 +1,315 @@
1
+ from typing import Sequence, Union
2
+
3
+ import numpy as np
4
+ import json
5
+
6
+
7
+ try:
8
+ from minerl.env import _singleagent
9
+ from minerl.herobraine.env_specs.human_controls import HumanControlEnvSpec
10
+ from minerl.herobraine.hero import handlers, mc, spaces
11
+ from minerl.herobraine.hero.handler import Handler
12
+ from minerl.herobraine.hero.handlers.agent.action import Action
13
+ from minerl.herobraine.hero.handlers.agent.start import InventoryAgentStart
14
+ from minerl.herobraine.hero.handlers.translation import TranslationHandler
15
+ except ImportError as e:
16
+ raise ImportError(
17
+ "The 'minerl' package is required to use RealPlancraft. "
18
+ "Please install it using 'pip install plancraft[full]' or 'pip install minerl'."
19
+ ) from e
20
+
21
+
22
+ from plancraft.environments.actions import RealAction
23
+
24
+
25
+ class InventoryCommandAction(Action):
26
+ """
27
+ Handler which lets agents programmatically interact with an open container
28
+
29
+ Using this - agents can move a chosen quantity of items from one slot to another.
30
+ """
31
+
32
+ def to_string(self):
33
+ return "inventory_command"
34
+
35
+ def xml_template(self) -> str:
36
+ return str("<InventoryCommands/>")
37
+
38
+ def __init__(self):
39
+ self._command = "inventory_command"
40
+ # first argument is the slot to take from
41
+ # second is the slot to put into
42
+ # third is the count to take
43
+ super().__init__(
44
+ self.command,
45
+ spaces.Tuple(
46
+ (
47
+ spaces.Discrete(46),
48
+ spaces.Discrete(46),
49
+ spaces.Discrete(64),
50
+ )
51
+ ),
52
+ )
53
+
54
+ def from_universal(self, x):
55
+ return np.array([0, 0, 0], dtype=np.int32)
56
+
57
+
58
+ class SmeltCommandAction(Action):
59
+ """
60
+ An action handler for smelting an item
61
+ We assume smelting is immediate.
62
+ @TODO: might be interesting to explore using the smelting time as an additional planning parameter.
63
+
64
+ Using this agents can smelt items in their inventory.
65
+ """
66
+
67
+ def __init__(self):
68
+ self._command = "smelt"
69
+ # first argument is the slot to take from
70
+ # second is the slot to put into
71
+ # third is the count to smelt
72
+ super().__init__(
73
+ self.command,
74
+ spaces.Tuple(
75
+ (
76
+ spaces.Discrete(46),
77
+ spaces.Discrete(46),
78
+ spaces.Discrete(64),
79
+ )
80
+ ),
81
+ )
82
+
83
+ def to_string(self):
84
+ return "smelt"
85
+
86
+ def xml_template(self) -> str:
87
+ return str("<SmeltCommands/>")
88
+
89
+ def from_universal(self, x):
90
+ return np.array([0, 0, 0], dtype=np.int32)
91
+
92
+
93
+ class InventoryResetAction(Action):
94
+ def __init__(self):
95
+ self._command = "inventory_reset"
96
+ super().__init__(self._command, spaces.Text([1]))
97
+
98
+ def to_string(self) -> str:
99
+ return "inventory_reset"
100
+
101
+ def to_hero(self, inventory_items: list[dict]):
102
+ return "{} {}".format(self._command, json.dumps(inventory_items))
103
+
104
+ def xml_template(self) -> str:
105
+ return "<InventoryResetCommands/>"
106
+
107
+ def from_universal(self, x):
108
+ return []
109
+
110
+
111
+ MINUTE = 20 * 60
112
+
113
+
114
+ class CustomInventoryAgentStart(InventoryAgentStart):
115
+ def __init__(self, inventory: list[dict[str, Union[str, int]]]):
116
+ super().__init__({item["slot"]: item for item in inventory})
117
+
118
+
119
+ class CraftingTableOnly(Handler):
120
+ def to_string(self):
121
+ return "start_with_crafting_table"
122
+
123
+ def xml_template(self) -> str:
124
+ return "<CraftingTableOnly>true</CraftingTableOnly>"
125
+
126
+
127
+ class InventoryObservation(TranslationHandler):
128
+ """
129
+ Handles GUI Workbench Observations for selected items
130
+ """
131
+
132
+ def to_string(self):
133
+ return "inventory"
134
+
135
+ def xml_template(self) -> str:
136
+ return str("""<ObservationFromFullInventory flat="false"/>""")
137
+
138
+ def __init__(self, item_list, _other="other"):
139
+ item_list = sorted(item_list)
140
+ super().__init__(
141
+ spaces.Dict(
142
+ spaces={
143
+ k: spaces.Box(
144
+ low=0,
145
+ high=2304,
146
+ shape=(),
147
+ dtype=np.int32,
148
+ normalizer_scale="log",
149
+ )
150
+ for k in item_list
151
+ }
152
+ )
153
+ )
154
+ self.num_items = len(item_list)
155
+ self.items = item_list
156
+
157
+ def add_to_mission_spec(self, mission_spec):
158
+ pass
159
+
160
+ def from_hero(self, info):
161
+ return info["inventory"]
162
+
163
+ def from_universal(self, obs):
164
+ raise NotImplementedError(
165
+ "from_universal not implemented in InventoryObservation"
166
+ )
167
+
168
+
169
+ class PlancraftBaseEnvSpec(HumanControlEnvSpec):
170
+ def __init__(
171
+ self,
172
+ symbolic_action_space=False,
173
+ symbolic_observation_space=False,
174
+ max_episode_steps=2 * MINUTE,
175
+ inventory: Sequence[dict] = (),
176
+ preferred_spawn_biome: str = "plains",
177
+ resolution=[260, 180],
178
+ ):
179
+ self.inventory = inventory
180
+ self.preferred_spawn_biome = preferred_spawn_biome
181
+ self.symbolic_action_space = symbolic_action_space
182
+ self.symbolic_observation_space = symbolic_observation_space
183
+
184
+ mode = "real"
185
+ if symbolic_action_space:
186
+ mode += "-symbolic-act"
187
+ else:
188
+ mode += "-real-act"
189
+
190
+ if symbolic_observation_space:
191
+ mode += "-symbolic-obs"
192
+
193
+ if symbolic_action_space:
194
+ cursor_size = 1
195
+ else:
196
+ cursor_size = 16
197
+
198
+ name = f"plancraft-{mode}-v0"
199
+ super().__init__(
200
+ name=name,
201
+ max_episode_steps=max_episode_steps,
202
+ resolution=resolution,
203
+ cursor_size_range=[cursor_size, cursor_size],
204
+ )
205
+
206
+ def create_agent_start(self) -> list[Handler]:
207
+ base_agent_start_handlers = super().create_agent_start()
208
+ return base_agent_start_handlers + [
209
+ CustomInventoryAgentStart(self.inventory),
210
+ handlers.PreferredSpawnBiome(self.preferred_spawn_biome),
211
+ handlers.DoneOnDeath(),
212
+ CraftingTableOnly(),
213
+ ]
214
+
215
+ def create_observables(self) -> list[TranslationHandler]:
216
+ if self.symbolic_observation_space:
217
+ return [
218
+ handlers.POVObservation(self.resolution),
219
+ InventoryObservation([item["slot"] for item in self.inventory]),
220
+ ]
221
+ return [handlers.POVObservation(self.resolution)]
222
+
223
+ def create_server_world_generators(self) -> list[Handler]:
224
+ # TODO the original biome forced is not implemented yet. Use this for now.
225
+ return [handlers.DefaultWorldGenerator(force_reset=True)]
226
+
227
+ def create_server_quit_producers(self) -> list[Handler]:
228
+ return [
229
+ handlers.ServerQuitFromTimeUp((self.max_episode_steps * mc.MS_PER_STEP)),
230
+ handlers.ServerQuitWhenAnyAgentFinishes(),
231
+ ]
232
+
233
+ def create_server_initial_conditions(self) -> list[Handler]:
234
+ return [
235
+ handlers.TimeInitialCondition(allow_passage_of_time=False),
236
+ handlers.SpawningInitialCondition(allow_spawning=True),
237
+ ]
238
+
239
+ def create_actionables(self) -> list[TranslationHandler]:
240
+ """
241
+ Symbolic env can move items around in the inventory using function
242
+ Real env can use camera/keyboard
243
+ """
244
+ # Camera and mouse
245
+ if self.symbolic_action_space:
246
+ return [
247
+ InventoryCommandAction(),
248
+ SmeltCommandAction(),
249
+ InventoryResetAction(),
250
+ ]
251
+ return [handlers.KeybasedCommandAction(v, v) for k, v in mc.KEYMAP.items()] + [
252
+ handlers.CameraAction(),
253
+ SmeltCommandAction(),
254
+ InventoryResetAction(),
255
+ ]
256
+
257
+ def is_from_folder(self, folder: str) -> bool:
258
+ return False
259
+
260
+ def create_agent_handlers(self) -> list[Handler]:
261
+ return []
262
+
263
+ def create_mission_handlers(self):
264
+ return []
265
+
266
+ def create_monitors(self):
267
+ return []
268
+
269
+ def create_rewardables(self):
270
+ return []
271
+
272
+ def create_server_decorators(self) -> list[Handler]:
273
+ return []
274
+
275
+ def determine_success_from_rewards(self, rewards: list) -> bool:
276
+ return False
277
+
278
+ def get_docstring(self):
279
+ return self.__class__.__doc__
280
+
281
+
282
+ class RealPlancraft(_singleagent._SingleAgentEnv):
283
+ def __init__(
284
+ self,
285
+ inventory: list[dict],
286
+ preferred_spawn_biome="plains",
287
+ symbolic_action_space=False,
288
+ symbolic_observation_space=True,
289
+ resolution=[512, 512],
290
+ crop=True,
291
+ ):
292
+ # NOTE: crop is only supported for resolution 512x512 (default)
293
+ self.crop = crop
294
+ self.resolution = resolution
295
+ env_spec = PlancraftBaseEnvSpec(
296
+ symbolic_action_space=symbolic_action_space,
297
+ symbolic_observation_space=symbolic_observation_space,
298
+ preferred_spawn_biome=preferred_spawn_biome,
299
+ inventory=inventory,
300
+ resolution=resolution,
301
+ )
302
+ super(RealPlancraft, self).__init__(env_spec=env_spec)
303
+ self.reset()
304
+
305
+ def step(self, action: RealAction | dict):
306
+ if not isinstance(action, dict):
307
+ action = action.to_action_dict()
308
+ obs, rew, done, info = super().step(action)
309
+ if "pov" in obs and self.crop and self.resolution == [512, 512]:
310
+ # crop at position x=174, y=170 with width=164 and height=173
311
+ obs["pov"] = obs["pov"][174 : 174 + 164, 170 : 168 + 173]
312
+ return obs, rew, done, info
313
+
314
+ def fast_reset(self, new_inventory: list[dict]):
315
+ super().step({"inventory_reset": new_inventory})