plancraft 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- environments/__init__.py +0 -0
- environments/actions.py +218 -0
- environments/env_real.py +315 -0
- environments/env_symbolic.py +215 -0
- environments/items.py +10 -0
- environments/planner.py +109 -0
- environments/recipes.py +542 -0
- environments/sampler.py +224 -0
- models/__init__.py +21 -0
- models/act.py +184 -0
- models/base.py +152 -0
- models/bbox_model.py +492 -0
- models/dummy.py +54 -0
- models/few_shot_images/__init__.py +16 -0
- models/generators.py +483 -0
- models/oam.py +284 -0
- models/oracle.py +268 -0
- models/prompts.py +158 -0
- models/react.py +98 -0
- models/utils.py +289 -0
- plancraft-0.1.0.dist-info/LICENSE +21 -0
- plancraft-0.1.0.dist-info/METADATA +53 -0
- plancraft-0.1.0.dist-info/RECORD +26 -0
- plancraft-0.1.0.dist-info/WHEEL +5 -0
- plancraft-0.1.0.dist-info/top_level.txt +3 -0
- train/dataset.py +187 -0
environments/__init__.py
ADDED
File without changes
|
environments/actions.py
ADDED
@@ -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]
|
environments/env_real.py
ADDED
@@ -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})
|