miniworld-maze 1.1.0__py3-none-any.whl → 1.2.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.

Potentially problematic release.


This version of miniworld-maze might be problematic. Click here for more details.

@@ -15,17 +15,29 @@ Main modules:
15
15
  - tools: Observation generation and utilities
16
16
  """
17
17
 
18
+ import os
19
+ import warnings
20
+
21
+ # Set PYGLET_HEADLESS=1 by default if not already set
22
+ if "PYGLET_HEADLESS" not in os.environ:
23
+ os.environ["PYGLET_HEADLESS"] = "1"
24
+ warnings.warn(
25
+ "Automatically set PYGLET_HEADLESS=1 for headless rendering. "
26
+ "Set PYGLET_HEADLESS=0 before importing miniworld_maze to override this behavior.",
27
+ UserWarning,
28
+ stacklevel=2
29
+ )
30
+
18
31
  from .core import ObservationLevel
19
- from .environments.factory import (
20
- NineRoomsEnvironmentWrapper,
21
- )
22
32
  from .environments.nine_rooms import NineRooms
23
33
  from .environments.spiral_nine_rooms import SpiralNineRooms
24
34
  from .environments.twenty_five_rooms import TwentyFiveRooms
25
35
 
26
- __version__ = "1.0.0"
36
+ # Import factory to register environments
37
+ from .environments import factory # noqa: F401
38
+
39
+ __version__ = "1.1.0"
27
40
  __all__ = [
28
- "NineRoomsEnvironmentWrapper",
29
41
  "NineRooms",
30
42
  "SpiralNineRooms",
31
43
  "TwentyFiveRooms",
@@ -57,23 +57,64 @@ DEFAULT_BENCHMARK_STEPS: Final[int] = 100
57
57
  DEFAULT_WARMUP_STEPS: Final[int] = 10
58
58
 
59
59
  # ========================
60
- # OBSERVATION TEST POSITIONS
60
+ # TEXTURE THEMES
61
61
  # ========================
62
62
 
63
+ class TextureThemes:
64
+ """Pre-defined texture themes for different environments."""
65
+
66
+ NINE_ROOMS = [
67
+ "beige",
68
+ "lightbeige",
69
+ "lightgray",
70
+ "copperred",
71
+ "skyblue",
72
+ "lightcobaltgreen",
73
+ "oakbrown",
74
+ "navyblue",
75
+ "cobaltgreen",
76
+ ]
77
+
78
+ SPIRAL_NINE_ROOMS = [
79
+ "beige",
80
+ "lightbeige",
81
+ "lightgray",
82
+ "copperred",
83
+ "skyblue",
84
+ "lightcobaltgreen",
85
+ "oakbrown",
86
+ "navyblue",
87
+ "cobaltgreen",
88
+ ]
89
+
90
+ TWENTY_FIVE_ROOMS = [
91
+ "crimson",
92
+ "beanpaste",
93
+ "cobaltgreen",
94
+ "lightnavyblue",
95
+ "skyblue",
96
+ "lightcobaltgreen",
97
+ "oakbrown",
98
+ "copperred",
99
+ "lightgray",
100
+ "lime",
101
+ "turquoise",
102
+ "violet",
103
+ "beige",
104
+ "morningglory",
105
+ "silver",
106
+ "magenta",
107
+ "sunnyyellow",
108
+ "blueberry",
109
+ "lightbeige",
110
+ "seablue",
111
+ "lemongrass",
112
+ "orchid",
113
+ "redbean",
114
+ "orange",
115
+ "realblueberry",
116
+ ]
63
117
 
64
- # Convenient position calculations for standard room layouts
65
- class RoomPositions:
66
- """Pre-calculated positions for standard room layouts."""
67
-
68
- # NineRooms (3x3) strategic positions
69
- NINE_ROOMS_CENTER: Final[tuple[float, float, float]] = (22.5, 0.0, 22.5)
70
- NINE_ROOMS_TOP_LEFT: Final[tuple[float, float, float]] = (7.5, 0.0, 7.5)
71
- NINE_ROOMS_BOTTOM_RIGHT: Final[tuple[float, float, float]] = (37.5, 0.0, 37.5)
72
-
73
- # TwentyFiveRooms (5x5) strategic positions
74
- TWENTY_FIVE_CENTER: Final[tuple[float, float, float]] = (75.0, 0.0, 75.0)
75
- TWENTY_FIVE_CORNER: Final[tuple[float, float, float]] = (37.5, 0.0, 37.5)
76
- TWENTY_FIVE_FAR_CORNER: Final[tuple[float, float, float]] = (112.5, 0.0, 112.5)
77
118
 
78
119
 
79
120
  # ========================
@@ -1,4 +1,4 @@
1
1
  # Core module cleaned up - imports removed since files were consolidated
2
2
 
3
3
  # Import the envs module so that envs register themselves
4
- from . import envs
4
+ from . import envs as envs
@@ -332,34 +332,32 @@ class UnifiedMiniWorldEnv(gym.Env):
332
332
  self._render_static()
333
333
 
334
334
  # Generate the first camera image
335
- obs = self._generate_observation()
335
+ obs = self._generate_observation(self.obs_level)
336
336
 
337
337
  # Generate additional observations for info dictionary if specified
338
338
  info = {}
339
339
  if self.info_obs is not None:
340
340
  for obs_level in self.info_obs:
341
- # Temporarily change obs_level to generate the desired observation
342
- original_obs_level = self.obs_level
343
- self.obs_level = obs_level
344
- info_obs = self._generate_observation()
345
- self.obs_level = original_obs_level
341
+ # Generate observation with the specified level
342
+ info_obs = self._generate_observation(observation_level=obs_level)
346
343
  # Use the observation level name as key
347
344
  info[str(obs_level)] = info_obs
348
345
 
349
346
  # Return first observation with info dict for Gymnasium compatibility
350
347
  return obs, info
351
348
 
352
- def _generate_observation(self, render_agent: bool = None):
353
- """Generate observation based on current observation level.
349
+ def _generate_observation(self, observation_level, render_agent: bool = None):
350
+ """Generate observation based on specified observation level.
354
351
 
355
352
  Args:
353
+ observation_level: Observation level to use.
356
354
  render_agent: Whether to render the agent in the observation.
357
355
  If None, uses default behavior based on observation level.
358
356
  """
359
357
  # Import ObservationLevel here to avoid circular imports
360
358
  from ..observation_types import ObservationLevel
361
359
 
362
- if self.obs_level == ObservationLevel.TOP_DOWN_PARTIAL:
360
+ if observation_level == ObservationLevel.TOP_DOWN_PARTIAL:
363
361
  if self.agent_mode == "empty":
364
362
  # Agent mode 'empty' always renders without agent
365
363
  render_ag = False
@@ -371,19 +369,19 @@ class UnifiedMiniWorldEnv(gym.Env):
371
369
  render_ag = True
372
370
  return self.render_top_view(POMDP=True, render_ag=render_ag)
373
371
 
374
- elif self.obs_level == ObservationLevel.TOP_DOWN_FULL:
372
+ elif observation_level == ObservationLevel.TOP_DOWN_FULL:
375
373
  # Use explicit render_agent parameter or default to True
376
374
  render_ag = render_agent if render_agent is not None else True
377
375
  return self.render_top_view(POMDP=False, render_ag=render_ag)
378
376
 
379
- elif self.obs_level == ObservationLevel.FIRST_PERSON:
377
+ elif observation_level == ObservationLevel.FIRST_PERSON:
380
378
  # First person view doesn't include the agent anyway
381
379
  return self.render_obs()
382
380
 
383
381
  else:
384
382
  valid_levels = list(ObservationLevel)
385
383
  raise ValueError(
386
- f"Invalid obs_level {self.obs_level}. Must be one of {valid_levels}"
384
+ f"Invalid obs_level {observation_level}. Must be one of {valid_levels}"
387
385
  )
388
386
 
389
387
  def _calculate_carried_object_position(self, agent_pos, ent):
@@ -507,7 +505,7 @@ class UnifiedMiniWorldEnv(gym.Env):
507
505
  self._process_action(action)
508
506
 
509
507
  # Generate observation
510
- observation = self._generate_observation()
508
+ observation = self._generate_observation(self.obs_level)
511
509
 
512
510
  # Calculate step results
513
511
  reward, terminated, info = self._calculate_step_results(observation)
@@ -586,11 +584,8 @@ class UnifiedMiniWorldEnv(gym.Env):
586
584
  info = {}
587
585
  if self.info_obs is not None:
588
586
  for obs_level in self.info_obs:
589
- # Temporarily change obs_level to generate the desired observation
590
- original_obs_level = self.obs_level
591
- self.obs_level = obs_level
592
- info_obs = self._generate_observation()
593
- self.obs_level = original_obs_level
587
+ # Generate observation with the specified level
588
+ info_obs = self._generate_observation(observation_level=obs_level)
594
589
  # Use the observation level name as key
595
590
  info[str(obs_level)] = info_obs
596
591
 
@@ -1,14 +1,12 @@
1
1
  """Nine Rooms environment implementations."""
2
2
 
3
3
  from .base_grid_rooms import GridRoomsEnvironment
4
- from .factory import NineRoomsEnvironmentWrapper
5
4
  from .nine_rooms import NineRooms
6
5
  from .spiral_nine_rooms import SpiralNineRooms
7
6
  from .twenty_five_rooms import TwentyFiveRooms
8
7
 
9
8
  __all__ = [
10
9
  "GridRoomsEnvironment",
11
- "NineRoomsEnvironmentWrapper",
12
10
  "NineRooms",
13
11
  "SpiralNineRooms",
14
12
  "TwentyFiveRooms",
@@ -77,7 +77,8 @@ class GridRoomsEnvironment(UnifiedMiniWorldEnv):
77
77
 
78
78
  # Validate and set textures
79
79
  assert len(textures) == self.total_rooms, (
80
- f"Textures for floor should be same as the number of the rooms ({self.total_rooms})"
80
+ f"Textures for floor should be same as the number of the rooms "
81
+ f"({self.total_rooms})"
81
82
  )
82
83
  self.textures = textures
83
84
 
@@ -226,16 +227,20 @@ class GridRoomsEnvironment(UnifiedMiniWorldEnv):
226
227
  obs, reward, terminated, truncated, info = super().step(action)
227
228
 
228
229
  # Check if goal is achieved
229
- if self.is_goal_achieved():
230
+ if self._is_goal_achieved():
230
231
  terminated = True
231
232
  reward = 1.0 # Positive reward for achieving goal
232
233
 
234
+ # Add agent and goal positions to info dictionary
235
+ agent_pos = self.agent.pos
236
+ info["agent_position"] = np.array([agent_pos[0], agent_pos[2]]) # x, z
237
+
238
+ if hasattr(self, "_current_goal_position"):
239
+ goal_pos = self._current_goal_position
240
+ info["goal_position"] = np.array([goal_pos[0], goal_pos[2]]) # x, z
241
+
233
242
  # Return observation as dict
234
- obs_dict = {
235
- "observation": obs,
236
- "desired_goal": self.desired_goal,
237
- "achieved_goal": obs,
238
- }
243
+ obs_dict = self._build_observation_dict(obs)
239
244
  return obs_dict, reward, terminated, truncated, info
240
245
 
241
246
  def reset(self, seed=None, options=None, pos=None):
@@ -254,17 +259,21 @@ class GridRoomsEnvironment(UnifiedMiniWorldEnv):
254
259
  obs, info = super().reset(seed=seed, options=options, pos=pos)
255
260
 
256
261
  # Generate goal
257
- self.desired_goal = self.get_goal()
262
+ self.desired_goal = self._get_goal()
263
+
264
+ # Add agent and goal positions to info dictionary
265
+ agent_pos = self.agent.pos
266
+ info["agent_position"] = np.array([agent_pos[0], agent_pos[2]]) # x, z
267
+
268
+ if hasattr(self, "_current_goal_position"):
269
+ goal_pos = self._current_goal_position
270
+ info["goal_position"] = np.array([goal_pos[0], goal_pos[2]]) # x, z
258
271
 
259
272
  # Return observation as dict with desired_goal and achieved_goal
260
- obs_dict = {
261
- "observation": obs,
262
- "desired_goal": self.desired_goal,
263
- "achieved_goal": obs,
264
- }
273
+ obs_dict = self._build_observation_dict(obs)
265
274
  return obs_dict, info
266
275
 
267
- def get_goal(self):
276
+ def _get_goal(self):
268
277
  """
269
278
  Generate a goal by randomly selecting a room and goal position.
270
279
 
@@ -318,7 +327,7 @@ class GridRoomsEnvironment(UnifiedMiniWorldEnv):
318
327
 
319
328
  return obs
320
329
 
321
- def is_goal_achieved(self, pos=None, threshold=0.5):
330
+ def _is_goal_achieved(self, pos=None, threshold=0.5):
322
331
  """
323
332
  Check if the agent has achieved the current goal.
324
333
 
@@ -341,3 +350,66 @@ class GridRoomsEnvironment(UnifiedMiniWorldEnv):
341
350
  distance = np.linalg.norm(pos_array - goal_array)
342
351
 
343
352
  return bool(distance < threshold)
353
+
354
+ @staticmethod
355
+ def _generate_goal_positions(
356
+ grid_size: int, room_size: Union[int, float], goals_per_room: int = 2
357
+ ) -> List[List[List[float]]]:
358
+ """
359
+ Generate goal positions for grid layout.
360
+ Args:
361
+ grid_size: Size of the grid (e.g., 3 for 3x3, 5 for 5x5)
362
+ room_size: Size of each room
363
+ goals_per_room: Number of goals per room (1 or 2)
364
+ Returns:
365
+ List of goal positions for each room
366
+ """
367
+ goal_positions = []
368
+ for i in range(grid_size): # rows
369
+ for j in range(grid_size): # columns
370
+ center_x = room_size * j + room_size / 2
371
+ center_z = room_size * i + room_size / 2
372
+ if goals_per_room == 1:
373
+ # One goal per room at the center
374
+ goal_positions.append([[center_x, 0.0, center_z]])
375
+ else:
376
+ # Two goals per room: center-left and center-right
377
+ goal_positions.append([
378
+ [center_x - 1.0, 0.0, center_z], # left goal
379
+ [center_x + 1.0, 0.0, center_z], # right goal
380
+ ])
381
+ return goal_positions
382
+
383
+ def get_extent(self, padding: float = 1.0) -> Tuple[float, float, float, float]:
384
+ """
385
+ Get the scene extent for use with matplotlib imshow.
386
+
387
+ Returns the scene bounds with padding in the format expected by
388
+ matplotlib's imshow(extent=...) parameter: (left, right, bottom, top).
389
+
390
+ Args:
391
+ padding: Padding to add around environment bounds (default: 1.0)
392
+
393
+ Returns:
394
+ Tuple[float, float, float, float]: (min_x, max_x, min_z, max_z) with padding
395
+ """
396
+ return (
397
+ self.min_x - padding,
398
+ self.max_x + padding,
399
+ self.min_z - padding,
400
+ self.max_z + padding
401
+ )
402
+
403
+ def _build_observation_dict(self, obs: np.ndarray) -> dict:
404
+ """
405
+ Build the standard observation dictionary format.
406
+ Args:
407
+ obs: The observation array
408
+ Returns:
409
+ Dictionary with observation, desired_goal, and achieved_goal
410
+ """
411
+ return {
412
+ "observation": obs,
413
+ "desired_goal": self.desired_goal,
414
+ "achieved_goal": obs,
415
+ }
@@ -1,105 +1,42 @@
1
- """Factory for creating Nine Rooms environment variants."""
2
-
3
- from typing import List
4
-
5
- import gymnasium as gym
6
- import numpy as np
1
+ """Gymnasium environment registrations for Nine Rooms environment variants."""
7
2
 
3
+ from gymnasium.envs.registration import register
8
4
  from ..core import ObservationLevel
9
5
  from ..core.constants import FACTORY_DOOR_SIZE, FACTORY_ROOM_SIZE
10
- from .nine_rooms import NineRooms
11
- from .spiral_nine_rooms import SpiralNineRooms
12
- from .twenty_five_rooms import TwentyFiveRooms
13
-
14
-
15
- class NineRoomsEnvironmentWrapper(gym.Wrapper):
16
- """Unified wrapper for all Nine Rooms environment variants."""
17
-
18
- def __init__(
19
- self,
20
- variant="NineRooms",
21
- obs_level=ObservationLevel.TOP_DOWN_PARTIAL,
22
- continuous=False,
23
- size=64,
24
- room_size=FACTORY_ROOM_SIZE,
25
- door_size=FACTORY_DOOR_SIZE,
26
- agent_mode=None,
27
- info_obs: List[ObservationLevel] = None,
28
- ):
29
- """
30
- Create a Nine Rooms environment variant.
31
-
32
- Args:
33
- variant: Environment variant ("NineRooms", "SpiralNineRooms", "TwentyFiveRooms")
34
- obs_level: Observation level (ObservationLevel enum)
35
- continuous: Whether to use continuous actions
36
- size: Observation image size (rendered directly at this size to avoid resizing)
37
- room_size: Size of each room in environment units
38
- door_size: Size of doors between rooms
39
- agent_mode: Agent rendering mode ('empty', 'circle', 'triangle', or None for default)
40
- info_obs: List of observation levels to include in info dictionary
41
- """
42
- self.variant = variant
43
-
44
- # Select the appropriate environment class
45
- env_classes = {
46
- "NineRooms": NineRooms,
47
- "SpiralNineRooms": SpiralNineRooms,
48
- "TwentyFiveRooms": TwentyFiveRooms,
49
- }
50
-
51
- if variant not in env_classes:
52
- raise ValueError(
53
- f"Unknown variant '{variant}'. Available: {list(env_classes.keys())}"
54
- )
55
-
56
- env_class = env_classes[variant]
57
-
58
- # Create base environment with direct rendering size
59
- base_env = env_class(
60
- room_size=room_size,
61
- door_size=door_size,
62
- obs_level=obs_level,
63
- continuous=continuous,
64
- obs_width=size,
65
- obs_height=size,
66
- agent_mode=agent_mode,
67
- info_obs=info_obs,
68
- )
69
-
70
- # Apply wrappers - no resize needed since we render at target size
71
-
72
- # Initialize gym.Wrapper with the base environment
73
- super().__init__(base_env)
74
-
75
- def render_on_pos(self, pos):
76
- """Render observation from a specific position."""
77
- # Get access to the base environment
78
- base_env = self.env
79
- while hasattr(base_env, "env") or hasattr(base_env, "_env"):
80
- if hasattr(base_env, "env"):
81
- base_env = base_env.env
82
- elif hasattr(base_env, "_env"):
83
- base_env = base_env._env
84
- else:
85
- break
86
-
87
- # Store original position
88
- original_pos = base_env.agent.pos.copy()
89
-
90
- # Move agent to target position
91
- base_env.place_agent(pos=pos)
92
-
93
- # Get first-person observation from the agent's perspective at this position
94
- obs = base_env.render_obs()
95
-
96
- # Restore original position
97
- base_env.place_agent(pos=original_pos)
98
-
99
- # Apply wrapper transformations manually for consistency
100
- # Convert to PyTorch format (CHW) - no resize needed since we render at target size
101
- obs = np.transpose(obs, (2, 0, 1))
102
-
103
- return obs
104
-
105
6
 
7
+ # Register environment variants with factory defaults matching the original wrapper
8
+ register(
9
+ id="NineRooms-v0",
10
+ entry_point="miniworld_maze.environments.nine_rooms:NineRooms",
11
+ max_episode_steps=1000,
12
+ kwargs={
13
+ "room_size": FACTORY_ROOM_SIZE,
14
+ "door_size": FACTORY_DOOR_SIZE,
15
+ "obs_level": ObservationLevel.TOP_DOWN_PARTIAL,
16
+ "agent_mode": None, # becomes "empty" by default
17
+ },
18
+ )
19
+
20
+ register(
21
+ id="SpiralNineRooms-v0",
22
+ entry_point="miniworld_maze.environments.spiral_nine_rooms:SpiralNineRooms",
23
+ max_episode_steps=1000,
24
+ kwargs={
25
+ "room_size": FACTORY_ROOM_SIZE,
26
+ "door_size": FACTORY_DOOR_SIZE,
27
+ "obs_level": ObservationLevel.TOP_DOWN_PARTIAL,
28
+ "agent_mode": None,
29
+ },
30
+ )
31
+
32
+ register(
33
+ id="TwentyFiveRooms-v0",
34
+ entry_point="miniworld_maze.environments.twenty_five_rooms:TwentyFiveRooms",
35
+ max_episode_steps=1000,
36
+ kwargs={
37
+ "room_size": FACTORY_ROOM_SIZE,
38
+ "door_size": FACTORY_DOOR_SIZE,
39
+ "obs_level": ObservationLevel.TOP_DOWN_PARTIAL,
40
+ "agent_mode": None,
41
+ },
42
+ )
@@ -1,6 +1,7 @@
1
1
  """NineRooms environment implementation."""
2
2
 
3
3
  from ..core import ObservationLevel
4
+ from ..core.constants import TextureThemes
4
5
  from .base_grid_rooms import GridRoomsEnvironment
5
6
 
6
7
 
@@ -46,31 +47,12 @@ class NineRooms(GridRoomsEnvironment):
46
47
  (6, 7),
47
48
  (7, 8),
48
49
  ]
49
- default_textures = [
50
- "beige",
51
- "lightbeige",
52
- "lightgray",
53
- "copperred",
54
- "skyblue",
55
- "lightcobaltgreen",
56
- "oakbrown",
57
- "navyblue",
58
- "cobaltgreen",
59
- ]
50
+ default_textures = TextureThemes.NINE_ROOMS
60
51
 
61
52
  # Initialize goal positions for each room (2 goals per room)
62
- goal_positions = []
63
- for i in range(3): # rows
64
- for j in range(3): # columns
65
- center_x = room_size * j + room_size / 2
66
- center_z = room_size * i + room_size / 2
67
- # Two goals per room: center-left and center-right
68
- goal_positions.append(
69
- [
70
- [center_x - 1.0, 0.0, center_z], # left goal
71
- [center_x + 1.0, 0.0, center_z], # right goal
72
- ]
73
- )
53
+ goal_positions = GridRoomsEnvironment._generate_goal_positions(
54
+ 3, room_size, goals_per_room=2
55
+ )
74
56
 
75
57
  super().__init__(
76
58
  grid_size=3,
@@ -1,6 +1,7 @@
1
1
  """SpiralNineRooms environment implementation."""
2
2
 
3
3
  from ..core import ObservationLevel
4
+ from ..core.constants import TextureThemes
4
5
  from .base_grid_rooms import GridRoomsEnvironment
5
6
 
6
7
 
@@ -42,31 +43,12 @@ class SpiralNineRooms(GridRoomsEnvironment):
42
43
  (6, 7),
43
44
  (7, 8),
44
45
  ]
45
- default_textures = [
46
- "beige",
47
- "lightbeige",
48
- "lightgray",
49
- "copperred",
50
- "skyblue",
51
- "lightcobaltgreen",
52
- "oakbrown",
53
- "navyblue",
54
- "cobaltgreen",
55
- ]
46
+ default_textures = TextureThemes.SPIRAL_NINE_ROOMS
56
47
 
57
48
  # Initialize goal positions for each room (2 goals per room)
58
- goal_positions = []
59
- for i in range(3): # rows
60
- for j in range(3): # columns
61
- center_x = room_size * j + room_size / 2
62
- center_z = room_size * i + room_size / 2
63
- # Two goals per room: center-left and center-right
64
- goal_positions.append(
65
- [
66
- [center_x - 1.0, 0.0, center_z], # left goal
67
- [center_x + 1.0, 0.0, center_z], # right goal
68
- ]
69
- )
49
+ goal_positions = GridRoomsEnvironment._generate_goal_positions(
50
+ 3, room_size, goals_per_room=2
51
+ )
70
52
 
71
53
  super().__init__(
72
54
  grid_size=3,
@@ -1,6 +1,7 @@
1
1
  """TwentyFiveRooms environment implementation."""
2
2
 
3
3
  from ..core import ObservationLevel
4
+ from ..core.constants import TextureThemes
4
5
  from .base_grid_rooms import GridRoomsEnvironment
5
6
 
6
7
 
@@ -78,46 +79,12 @@ class TwentyFiveRooms(GridRoomsEnvironment):
78
79
  (22, 23),
79
80
  (23, 24),
80
81
  ]
81
- default_textures = [
82
- "crimson",
83
- "beanpaste",
84
- "cobaltgreen",
85
- "lightnavyblue",
86
- "skyblue",
87
- "lightcobaltgreen",
88
- "oakbrown",
89
- "copperred",
90
- "lightgray",
91
- "lime",
92
- "turquoise",
93
- "violet",
94
- "beige",
95
- "morningglory",
96
- "silver",
97
- "magenta",
98
- "sunnyyellow",
99
- "blueberry",
100
- "lightbeige",
101
- "seablue",
102
- "lemongrass",
103
- "orchid",
104
- "redbean",
105
- "orange",
106
- "realblueberry",
107
- ]
82
+ default_textures = TextureThemes.TWENTY_FIVE_ROOMS
108
83
 
109
84
  # Initialize goal positions for each room (1 goal per room at center)
110
- goal_positions = []
111
- for i in range(5): # rows
112
- for j in range(5): # columns
113
- center_x = room_size * j + room_size / 2
114
- center_z = room_size * i + room_size / 2
115
- # One goal per room at the center
116
- goal_positions.append(
117
- [
118
- [center_x, 0.0, center_z],
119
- ]
120
- )
85
+ goal_positions = GridRoomsEnvironment._generate_goal_positions(
86
+ 5, room_size, goals_per_room=1
87
+ )
121
88
 
122
89
  super().__init__(
123
90
  grid_size=5,
@@ -1,5 +1,3 @@
1
1
  """Tools for Nine Rooms environments."""
2
2
 
3
- from .generate_observations import generate_observations, main
4
-
5
- __all__ = ["generate_observations", "main"]
3
+ __all__ = []
@@ -0,0 +1,286 @@
1
+ """Utility functions for miniworld_maze package."""
2
+
3
+ from typing import Tuple, Union
4
+ import numpy as np
5
+
6
+
7
+ def environment_to_pixel_coords(
8
+ env_pos: np.ndarray,
9
+ env_min: np.ndarray,
10
+ env_max: np.ndarray,
11
+ image_size: Union[int, Tuple[int, int]],
12
+ ) -> Tuple[int, int]:
13
+ """
14
+ Convert environment coordinates to pixel coordinates.
15
+
16
+ Args:
17
+ env_pos: Environment position as ndarray [x, z] or [x, y]
18
+ env_min: Environment minimum bounds as ndarray [min_x, min_z] or [min_x, min_y]
19
+ env_max: Environment maximum bounds as ndarray [max_x, max_z] or [max_x, max_y]
20
+ image_size: Image size (width, height) or single size for square image
21
+
22
+ Returns:
23
+ Tuple of (pixel_x, pixel_y) coordinates
24
+ """
25
+ env_x, env_y = env_pos[:2]
26
+ env_min_x, env_min_y = env_min[:2]
27
+ env_max_x, env_max_y = env_max[:2]
28
+
29
+ if isinstance(image_size, int):
30
+ width = height = image_size
31
+ else:
32
+ width, height = image_size
33
+
34
+ # Normalize to [0, 1] range and scale to pixel coordinates
35
+ pixel_x = int((env_x - env_min_x) / (env_max_x - env_min_x) * width)
36
+ pixel_y = int((env_y - env_min_y) / (env_max_y - env_min_y) * height)
37
+
38
+ return pixel_x, pixel_y
39
+
40
+
41
+ def pixel_to_environment_coords(
42
+ pixel_pos: np.ndarray,
43
+ env_min: np.ndarray,
44
+ env_max: np.ndarray,
45
+ image_size: Union[int, Tuple[int, int]],
46
+ ) -> Tuple[float, float]:
47
+ """
48
+ Convert pixel coordinates to environment coordinates.
49
+
50
+ Args:
51
+ pixel_pos: Pixel position as ndarray [x, y]
52
+ env_min: Environment minimum bounds as ndarray [min_x, min_z] or [min_x, min_y]
53
+ env_max: Environment maximum bounds as ndarray [max_x, max_z] or [max_x, max_y]
54
+ image_size: Image size (width, height) or single size for square image
55
+
56
+ Returns:
57
+ Tuple of (env_x, env_y) coordinates
58
+ """
59
+ pixel_x, pixel_y = pixel_pos[:2]
60
+ env_min_x, env_min_y = env_min[:2]
61
+ env_max_x, env_max_y = env_max[:2]
62
+
63
+ if isinstance(image_size, int):
64
+ width = height = image_size
65
+ else:
66
+ width, height = image_size
67
+
68
+ # Convert to normalized [0, 1] range and scale to environment coordinates
69
+ env_x = pixel_x / width * (env_max_x - env_min_x) + env_min_x
70
+ env_y = pixel_y / height * (env_max_y - env_min_y) + env_min_y
71
+
72
+ return env_x, env_y
73
+
74
+
75
+ def clamp_to_bounds(
76
+ value: Union[int, float, np.ndarray],
77
+ min_val: Union[int, float, np.ndarray],
78
+ max_val: Union[int, float, np.ndarray],
79
+ ) -> Union[int, float, np.ndarray]:
80
+ """
81
+ Clamp a value or array of values to specified bounds.
82
+
83
+ Args:
84
+ value: Value(s) to clamp (scalar or ndarray)
85
+ min_val: Minimum bound(s) (scalar or ndarray)
86
+ max_val: Maximum bound(s) (scalar or ndarray)
87
+
88
+ Returns:
89
+ Clamped value(s)
90
+ """
91
+ if isinstance(value, np.ndarray):
92
+ return np.clip(value, min_val, max_val)
93
+ else:
94
+ return max(min_val, min(max_val, value))
95
+
96
+
97
+ def clamp_pixel_coords(
98
+ pixel_x: int, pixel_y: int, image_size: Union[int, Tuple[int, int]]
99
+ ) -> Tuple[int, int]:
100
+ """
101
+ Clamp pixel coordinates to image bounds.
102
+
103
+ Args:
104
+ pixel_x: X pixel coordinate
105
+ pixel_y: Y pixel coordinate
106
+ image_size: Image size (width, height) or single size for square image
107
+
108
+ Returns:
109
+ Tuple of clamped (pixel_x, pixel_y) coordinates
110
+ """
111
+ if isinstance(image_size, int):
112
+ width = height = image_size
113
+ else:
114
+ width, height = image_size
115
+
116
+ clamped_x = max(0, min(width - 1, pixel_x))
117
+ clamped_y = max(0, min(height - 1, pixel_y))
118
+
119
+ return clamped_x, clamped_y
120
+
121
+
122
+ def normalize_coordinates(
123
+ coords: np.ndarray,
124
+ min_bounds: np.ndarray,
125
+ max_bounds: np.ndarray,
126
+ ) -> Tuple[float, float]:
127
+ """
128
+ Normalize coordinates to [0, 1] range based on given bounds.
129
+
130
+ Args:
131
+ coords: Coordinates to normalize as ndarray
132
+ min_bounds: Minimum bounds as ndarray
133
+ max_bounds: Maximum bounds as ndarray
134
+
135
+ Returns:
136
+ Normalized coordinates as (x, y) tuple
137
+ """
138
+ x, y = coords[:2]
139
+ min_x, min_y = min_bounds[:2]
140
+ max_x, max_y = max_bounds[:2]
141
+
142
+ norm_x = (x - min_x) / (max_x - min_x)
143
+ norm_y = (y - min_y) / (max_y - min_y)
144
+
145
+ return norm_x, norm_y
146
+
147
+
148
+ def denormalize_coordinates(
149
+ normalized_coords: np.ndarray,
150
+ min_bounds: np.ndarray,
151
+ max_bounds: np.ndarray,
152
+ ) -> Tuple[float, float]:
153
+ """
154
+ Convert normalized [0, 1] coordinates back to original coordinate space.
155
+
156
+ Args:
157
+ normalized_coords: Normalized coordinates in [0, 1] range as ndarray
158
+ min_bounds: Original minimum bounds as ndarray
159
+ max_bounds: Original maximum bounds as ndarray
160
+
161
+ Returns:
162
+ Denormalized coordinates as (x, y) tuple
163
+ """
164
+ norm_x, norm_y = normalized_coords[:2]
165
+ min_x, min_y = min_bounds[:2]
166
+ max_x, max_y = max_bounds[:2]
167
+
168
+ x = norm_x * (max_x - min_x) + min_x
169
+ y = norm_y * (max_y - min_y) + min_y
170
+
171
+ return x, y
172
+
173
+
174
+ def get_environment_bounds(env) -> Tuple[Tuple[float, float], Tuple[float, float]]:
175
+ """
176
+ Extract environment bounds from an environment object.
177
+
178
+ Args:
179
+ env: Environment object (may be wrapped)
180
+
181
+ Returns:
182
+ Tuple of ((min_x, min_z), (max_x, max_z))
183
+ """
184
+ # Unwrap environment to access base environment
185
+ base_env = env
186
+ while hasattr(base_env, "env"):
187
+ base_env = base_env.env
188
+
189
+ min_bounds = (base_env.min_x, base_env.min_z)
190
+ max_bounds = (base_env.max_x, base_env.max_z)
191
+
192
+ return min_bounds, max_bounds
193
+
194
+
195
+ def calculate_view_size_from_bounds(
196
+ min_bounds: np.ndarray,
197
+ max_bounds: np.ndarray,
198
+ ) -> Tuple[float, float]:
199
+ """
200
+ Calculate view size (width, height) from coordinate bounds.
201
+
202
+ Args:
203
+ min_bounds: Minimum bounds as ndarray (min_x, min_y)
204
+ max_bounds: Maximum bounds as ndarray (max_x, max_y)
205
+
206
+ Returns:
207
+ Tuple of (width, height)
208
+ """
209
+ min_x, min_y = min_bounds[:2]
210
+ max_x, max_y = max_bounds[:2]
211
+
212
+ width = max_x - min_x
213
+ height = max_y - min_y
214
+
215
+ return width, height
216
+
217
+
218
+ def scale_coordinates(
219
+ coords: np.ndarray,
220
+ scale_factor: Union[float, Tuple[float, float]],
221
+ ) -> Tuple[float, float]:
222
+ """
223
+ Scale coordinates by a given factor.
224
+
225
+ Args:
226
+ coords: Coordinates to scale as ndarray
227
+ scale_factor: Scale factor (uniform) or (scale_x, scale_y)
228
+
229
+ Returns:
230
+ Scaled coordinates as (x, y) tuple
231
+ """
232
+ x, y = coords[:2]
233
+
234
+ if isinstance(scale_factor, (tuple, list)):
235
+ scale_x, scale_y = scale_factor
236
+ else:
237
+ scale_x = scale_y = scale_factor
238
+
239
+ return x * scale_x, y * scale_y
240
+
241
+
242
+ def distance_2d(
243
+ pos1: np.ndarray,
244
+ pos2: np.ndarray,
245
+ ) -> float:
246
+ """
247
+ Calculate 2D Euclidean distance between two positions.
248
+
249
+ Args:
250
+ pos1: First position as ndarray (x, y)
251
+ pos2: Second position as ndarray (x, y)
252
+
253
+ Returns:
254
+ Euclidean distance
255
+ """
256
+ x1, y1 = pos1[:2]
257
+ x2, y2 = pos2[:2]
258
+
259
+ return np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
260
+
261
+
262
+ def lerp_2d(
263
+ pos1: np.ndarray,
264
+ pos2: np.ndarray,
265
+ t: float,
266
+ ) -> Tuple[float, float]:
267
+ """
268
+ Linear interpolation between two 2D positions.
269
+
270
+ Args:
271
+ pos1: Start position as ndarray (x, y)
272
+ pos2: End position as ndarray (x, y)
273
+ t: Interpolation parameter [0, 1]
274
+
275
+ Returns:
276
+ Interpolated position as (x, y) tuple
277
+ """
278
+ x1, y1 = pos1[:2]
279
+ x2, y2 = pos2[:2]
280
+
281
+ t = clamp_to_bounds(t, 0.0, 1.0)
282
+
283
+ x = x1 + t * (x2 - x1)
284
+ y = y1 + t * (y2 - y1)
285
+
286
+ return x, y
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: miniworld-maze
3
- Version: 1.1.0
4
- Summary: Multi-room maze environments from the DrStrategy paper for reinforcement learning research
3
+ Version: 1.2.0
4
+ Summary: Multi-room maze environments from the DrStrategy paper. Provides NineRooms-v0, SpiralNineRooms-v0, and TwentyFiveRooms-v0 gymnasium environments.
5
5
  Keywords: reinforcement-learning,environment,gymnasium,multi-room-maze,drstrategy,maze-navigation,partial-observability,3d-environments
6
6
  Author: Tim Joseph
7
7
  Author-email: Tim Joseph <tim@mctigger.com>
@@ -46,9 +46,6 @@ Requires-Dist: opencv-python>=4.5.0,<5.0.0
46
46
  Requires-Dist: pillow>=8.0.0,<11.0.0
47
47
  Requires-Dist: pyopengl>=3.1.0,<4.0.0
48
48
  Requires-Dist: pyglet>=1.5.0,<2.0.0
49
- Requires-Dist: black>=22.0,<25.0 ; extra == 'dev'
50
- Requires-Dist: isort>=5.10,<6.0 ; extra == 'dev'
51
- Requires-Dist: flake8>=4.0,<7.0 ; extra == 'dev'
52
49
  Requires-Dist: ruff>=0.1.0,<1.0.0 ; extra == 'dev'
53
50
  Requires-Dist: build>=0.8.0,<2.0.0 ; extra == 'dev'
54
51
  Requires-Dist: twine>=4.0.0,<6.0.0 ; extra == 'dev'
@@ -83,6 +80,18 @@ pip install miniworld-maze
83
80
 
84
81
  ## Usage
85
82
 
83
+ ### Registered Environments
84
+
85
+ This package registers the following gymnasium environments:
86
+
87
+ | Environment ID | Description | Rooms | Max Steps |
88
+ |---|---|---|---|
89
+ | `NineRooms-v0` | Standard 3×3 grid with adjacent room connections | 9 | 1000 |
90
+ | `SpiralNineRooms-v0` | 3×3 grid with spiral connection pattern | 9 | 1000 |
91
+ | `TwentyFiveRooms-v0` | Large 5×5 grid with complex navigation | 25 | 1000 |
92
+
93
+ All environments use `TOP_DOWN_PARTIAL` observation level and factory default room/door sizes by default.
94
+
86
95
  ### Basic Usage
87
96
 
88
97
  See `examples/basic_usage.py` for a complete working example:
@@ -95,12 +104,13 @@ Basic usage example for miniworld-maze environments.
95
104
  This is a minimal example showing how to create and interact with the environment.
96
105
  """
97
106
 
98
- from miniworld_maze import NineRoomsEnvironmentWrapper
107
+ import gymnasium as gym
108
+ import miniworld_maze # noqa: F401
99
109
 
100
110
 
101
111
  def main():
102
- # Create environment
103
- env = NineRoomsEnvironmentWrapper(variant="NineRooms", size=64)
112
+ # Create environment using gymnasium registry
113
+ env = gym.make("NineRooms-v0", obs_width=64, obs_height=64)
104
114
  obs, info = env.reset()
105
115
 
106
116
  # obs is a dictionary containing:
@@ -212,14 +222,15 @@ Each environment supports three different observation modes:
212
222
  All environments can be customized with the following parameters:
213
223
 
214
224
  ```python
215
- from miniworld_maze import NineRoomsEnvironmentWrapper
216
- from miniworld_maze.core import ObservationLevel
225
+ import gymnasium as gym
226
+ from miniworld_maze import ObservationLevel
227
+ import miniworld_maze # noqa: F401
217
228
 
218
- env = NineRoomsEnvironmentWrapper(
219
- variant="NineRooms", # "NineRooms", "SpiralNineRooms", "TwentyFiveRooms"
229
+ env = gym.make(
230
+ "NineRooms-v0", # Environment variant
220
231
  obs_level=ObservationLevel.TOP_DOWN_PARTIAL, # Observation type
221
- continuous=False, # Use continuous actions
222
- size=64, # Observation image size (64x64)
232
+ obs_width=64, # Observation image width
233
+ obs_height=64, # Observation image height
223
234
  room_size=5, # Size of each room in environment units
224
235
  door_size=2, # Size of doors between rooms
225
236
  agent_mode="empty", # Agent rendering: "empty", "circle", "triangle"
@@ -1,8 +1,8 @@
1
- miniworld_maze/__init__.py,sha256=kz5QqyvwIV_XkjXw_yHRkhqlPSl8X1Usra3DF0Zy4_E,1053
1
+ miniworld_maze/__init__.py,sha256=Dfq3558bJ5kgxDZxPxvlHu1531zfBhtiDfKbVL_4zCE,1430
2
2
  miniworld_maze/core/__init__.py,sha256=5BA4WKXQjrG55TNaEid2JGrnf1KQniJZ1HhRqovM1Q0,293
3
- miniworld_maze/core/constants.py,sha256=GX1pSnaWGOkangYStxaBy7gIQhWxVKIbXnTSd-eA_vs,3962
3
+ miniworld_maze/core/constants.py,sha256=UXqmuvsT9ww1GljW74lWPPP9XxVmvti-8JfM_RBSoWQ,4314
4
4
  miniworld_maze/core/miniworld_gymnasium/README.md,sha256=kkZgkRKBdgixpot3uHuiBFlRIKRFIVBVfXwu68XTEv0,74
5
- miniworld_maze/core/miniworld_gymnasium/__init__.py,sha256=zgqRxYBKT9Jo06kqWusZ6OjHyz25AyW03gkyrpZqY1I,151
5
+ miniworld_maze/core/miniworld_gymnasium/__init__.py,sha256=ALOqr4SCVk9I9bmxyoU0Du8LUPE_57jIyZuV4QhOcBc,159
6
6
  miniworld_maze/core/miniworld_gymnasium/base_env.py,sha256=heoJxEw_GGEHSzU75MWgqRkWARY3lgTgwNE9Zp1P_YA,1795
7
7
  miniworld_maze/core/miniworld_gymnasium/entities/__init__.py,sha256=gbMJcFeasvlRSIuXWyOYO-MhW0JKKY5ZNxF5TMsWJ5c,262
8
8
  miniworld_maze/core/miniworld_gymnasium/entities/agent.py,sha256=U3Zt2Hk21rJSdzPCBj46-N930n7XAkA9qo_fKbezn2k,3153
@@ -260,19 +260,18 @@ miniworld_maze/core/miniworld_gymnasium/textures/white_1.png,sha256=wRrgs92I_Ids
260
260
  miniworld_maze/core/miniworld_gymnasium/textures/wood_1.png,sha256=XRZyIN34HFo14olbxRcsHGrzCAFqUlowc6nLR22IFBE,184713
261
261
  miniworld_maze/core/miniworld_gymnasium/textures/wood_2.png,sha256=qSDHB-ZO11JJLQuiQse-0edpbuTg1YO-eIBhdTvNUhc,93121
262
262
  miniworld_maze/core/miniworld_gymnasium/textures/wood_planks_1.png,sha256=E4SNN1s4yOtkLfZFQy905eip6KvDWnnPUrpS82FxMAg,847259
263
- miniworld_maze/core/miniworld_gymnasium/unified_env.py,sha256=oY2YXCZ-ro8kl6ie0h8TEBooWjB6bvmQu42-1tDsrw4,46689
263
+ miniworld_maze/core/miniworld_gymnasium/unified_env.py,sha256=B7kfG6axiL05o3kLiBvE6O1Q7kB8PaLdlAt1-Sksv0I,46529
264
264
  miniworld_maze/core/miniworld_gymnasium/utils.py,sha256=9cfpg4qYz-Esxvu8nTMPFJc-Tl0TRxTrX6cfg0YuK_o,1007
265
265
  miniworld_maze/core/miniworld_gymnasium/wrappers.py,sha256=cD0nGSJYNU96zoWv63aEiKd986POhtHfGGEpNpRL5ec,122
266
266
  miniworld_maze/core/observation_types.py,sha256=Co8mEIXzIgk0MLx6tqeBd1EE0PuZOL1gbZwobiEde08,1316
267
- miniworld_maze/environments/__init__.py,sha256=6O1G4vlhUSn8OuR46u-t4Wz3de8da76Tz21TsVlzBZo,415
268
- miniworld_maze/environments/base_grid_rooms.py,sha256=kpF6psn0YCfvA-q1vtU2KaqwmfQaPpX0w9jBTxt2qs8,11833
269
- miniworld_maze/environments/factory.py,sha256=g8qYF6UF9DsKExZIHhMsP8PX3b11kvmkLMwE2IpgBxc,3501
270
- miniworld_maze/environments/nine_rooms.py,sha256=0CzUkRy2hJZ4JEEj9E-6xCopseZ1u8iGCfKLaxwATGo,2368
271
- miniworld_maze/environments/spiral_nine_rooms.py,sha256=LB9wKQXp9S8pVd8JhM9zfsc5z4ksxsQxQKoEXrDgPhE,2316
272
- miniworld_maze/environments/twenty_five_rooms.py,sha256=ExF4Mt0B3pkaVQtBF7oaVxdj_9uaAkTFYCe83Lw8Evk,3448
273
- miniworld_maze/tools/__init__.py,sha256=PgkKMO21xnwIfJtD437P_RtkguOHoOSFV1ohsO-n7tc,150
274
- miniworld_maze/tools/generate_observations.py,sha256=-WWoex0KY6PSMs4BenMTepeE4tA2NYZ26jIQL2tYfZY,6781
275
- miniworld_maze-1.1.0.dist-info/WHEEL,sha256=NHRAbdxxzyL9K3IO2LjmlNqKSyPZnKv2BD16YYVKo18,79
276
- miniworld_maze-1.1.0.dist-info/entry_points.txt,sha256=Ue03NHCOQCiJ87tqfJSns29C3Dw02jsZXL_Auzw2pb4,91
277
- miniworld_maze-1.1.0.dist-info/METADATA,sha256=atsdko20fA_iqS8y2muB0dQkLDnmLpP4ppuTuNLfcxA,9270
278
- miniworld_maze-1.1.0.dist-info/RECORD,,
267
+ miniworld_maze/environments/__init__.py,sha256=DKld5MQU7x9eNL6BlxIettA44bCiIn2zIpYECDCNxoQ,331
268
+ miniworld_maze/environments/base_grid_rooms.py,sha256=j4LnyUv0YjHlCLfjqX3fyoXUOOyCr4e3_Pdj-sM3FHo,14753
269
+ miniworld_maze/environments/factory.py,sha256=Zk26JawsMgSLMgvSnQxFhQCD8yMH76HgqFtogzWFfqY,1333
270
+ miniworld_maze/environments/nine_rooms.py,sha256=Ct96cKtSt1_nLNI5RBUhwqdNUQq1rHfBJ3aB5Igbdow,1794
271
+ miniworld_maze/environments/spiral_nine_rooms.py,sha256=a_pUuv-ghez8h76Z7YsHkQoLXsQ-w9azKLjEjO4uKmA,1749
272
+ miniworld_maze/environments/twenty_five_rooms.py,sha256=MewKPDHDilscQGTT3aGRrSHvo4uFgHHAOrnJMrHaezQ,2598
273
+ miniworld_maze/tools/__init__.py,sha256=XiReXrJIcBKvDVyPZrKPq1mckJs0_WC7q_RmdXXcKHs,55
274
+ miniworld_maze/utils.py,sha256=HTOkfRq72oOC844gVXjWMH1ox7wdSxfCS6oTrWBw05Q,7523
275
+ miniworld_maze-1.2.0.dist-info/WHEEL,sha256=NHRAbdxxzyL9K3IO2LjmlNqKSyPZnKv2BD16YYVKo18,79
276
+ miniworld_maze-1.2.0.dist-info/METADATA,sha256=QHJBCKSCHQr2u-Utcck1qMOffK1Cc6T0sV2k6_H7OAE,9657
277
+ miniworld_maze-1.2.0.dist-info/RECORD,,
@@ -1,199 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Generate comprehensive observations for Nine Rooms environment variants.
4
- Supports: NineRooms, SpiralNineRooms, TwentyFiveRooms
5
- """
6
-
7
- import argparse
8
- import os
9
-
10
- import numpy as np
11
- from PIL import Image
12
-
13
- from ..core import FrameBuffer
14
- from ..environments.factory import NineRoomsEnvironmentWrapper
15
-
16
-
17
- def generate_observations(variant, output_dir=None, high_res_full_views=False):
18
- """Generate comprehensive observations for the specified environment variant."""
19
- if output_dir is None:
20
- output_dir = f"{variant.lower()}_observations"
21
-
22
- os.makedirs(output_dir, exist_ok=True)
23
-
24
- # Create environment
25
- env = NineRoomsEnvironmentWrapper(variant=variant, size=64)
26
-
27
- # Get base environment for direct render access
28
- base_env = getattr(env, "env", getattr(env, "_env", env))
29
- while hasattr(base_env, "env") or hasattr(base_env, "_env"):
30
- if hasattr(base_env, "env"):
31
- base_env = base_env.env
32
- elif hasattr(base_env, "_env"):
33
- base_env = base_env._env
34
- else:
35
- break
36
-
37
- # Reset environment
38
- obs, _ = env.reset(seed=42)
39
-
40
- # Create high-resolution frame buffer if requested
41
- high_res_fb = None
42
- if high_res_full_views:
43
- high_res_fb = FrameBuffer(512, 512, 8)
44
-
45
- # === FULL ENVIRONMENT OBSERVATIONS ===
46
-
47
- # 1. Full view with agent at starting position
48
- if high_res_full_views:
49
- full_view_start = base_env.render_top_view(
50
- frame_buffer=high_res_fb, POMDP=False
51
- )
52
- else:
53
- full_view_start = base_env.render_top_view(POMDP=False)
54
- Image.fromarray(full_view_start).save(f"{output_dir}/full_view_start.png")
55
-
56
- # 2. Full view without agent (clean maze view)
57
- if high_res_full_views:
58
- full_view_clean = base_env.render_top_view(
59
- frame_buffer=high_res_fb, POMDP=False, render_ag=False
60
- )
61
- else:
62
- full_view_clean = base_env.render_top_view(POMDP=False, render_ag=False)
63
- Image.fromarray(full_view_clean).save(f"{output_dir}/full_view_clean.png")
64
-
65
- # 3. Full view with agent in center
66
- center_x = (base_env.min_x + base_env.max_x) / 2
67
- center_z = (base_env.min_z + base_env.max_z) / 2
68
- base_env.place_agent(pos=[center_x, 0.0, center_z])
69
- if high_res_full_views:
70
- full_view_center = base_env.render_top_view(
71
- frame_buffer=high_res_fb, POMDP=False
72
- )
73
- else:
74
- full_view_center = base_env.render_top_view(POMDP=False)
75
- Image.fromarray(full_view_center).save(f"{output_dir}/full_view_center.png")
76
-
77
- # === PARTIAL OBSERVATIONS ===
78
-
79
- # Reset agent to start position
80
- base_env.place_agent(pos=[2.5, 0.0, 2.5])
81
-
82
- # 1. Partial view from starting position
83
- partial_start = base_env.render_top_view(POMDP=True)
84
- Image.fromarray(partial_start).save(f"{output_dir}/partial_start.png")
85
-
86
- # 2. Partial view from center
87
- base_env.place_agent(pos=[center_x, 0.0, center_z])
88
- partial_center = base_env.render_top_view(POMDP=True)
89
- Image.fromarray(partial_center).save(f"{output_dir}/partial_center.png")
90
-
91
- # 3. Partial view from corner/edge
92
- corner_x = base_env.max_x - 2.5
93
- corner_z = base_env.max_z - 2.5
94
- base_env.place_agent(pos=[corner_x, 0.0, corner_z])
95
- partial_corner = base_env.render_top_view(POMDP=True)
96
- Image.fromarray(partial_corner).save(f"{output_dir}/partial_corner.png")
97
-
98
- # 4. Partial view at strategic location
99
- if variant == "NineRooms":
100
- strategic_x, strategic_z = 15.0, 7.5 # Room boundary
101
- elif variant == "SpiralNineRooms":
102
- strategic_x, strategic_z = 22.5, 15.0 # Center of spiral
103
- else: # TwentyFiveRooms
104
- strategic_x, strategic_z = 37.5, 37.5 # Mid-outer area
105
-
106
- base_env.place_agent(pos=[strategic_x, 0.0, strategic_z])
107
- partial_strategic = base_env.render_top_view(POMDP=True)
108
- Image.fromarray(partial_strategic).save(f"{output_dir}/partial_strategic.png")
109
-
110
- # === WRAPPED OBSERVATIONS ===
111
-
112
- # Reset environment
113
- obs, _ = env.reset(seed=42)
114
-
115
- # 1. Standard gymnasium observation
116
- obs_hwc = np.transpose(obs, (1, 2, 0))
117
- Image.fromarray(obs_hwc).save(f"{output_dir}/gymnasium_standard.png")
118
-
119
- # 2. Observations after movement
120
- actions = [2, 2, 1] # move_forward, move_forward, turn_right
121
- action_names = ["move_forward", "move_forward", "turn_right"]
122
-
123
- for i, action in enumerate(actions):
124
- obs, _, _, _, _ = env.step(action)
125
- obs_hwc = np.transpose(obs, (1, 2, 0))
126
- Image.fromarray(obs_hwc).save(
127
- f"{output_dir}/gymnasium_step_{i + 1}_{action_names[i]}.png"
128
- )
129
-
130
- # === RENDER_ON_POS EXAMPLES ===
131
-
132
- # Define test positions based on variant
133
- if variant == "NineRooms":
134
- test_positions = [
135
- [7.5, 0.0, 7.5], # top-middle room center
136
- [37.5, 0.0, 37.5], # bottom-right room center
137
- [22.5, 0.0, 22.5], # environment center
138
- [7.5, 0.0, 22.5], # middle-left room center
139
- ]
140
- elif variant == "SpiralNineRooms":
141
- test_positions = [
142
- [7.5, 0.0, 7.5], # top-left room (spiral start)
143
- [37.5, 0.0, 37.5], # bottom-right room (spiral end)
144
- [22.5, 0.0, 7.5], # top-right room
145
- [7.5, 0.0, 37.5], # bottom-left room
146
- ]
147
- else: # TwentyFiveRooms
148
- test_positions = [
149
- [37.5, 0.0, 37.5], # room (1,1) - near corner
150
- [112.5, 0.0, 112.5], # room (4,4) - far corner
151
- [75.0, 0.0, 75.0], # center room (2,2)
152
- [37.5, 0.0, 112.5], # room (1,4) - opposite corner
153
- ]
154
-
155
- for i, pos in enumerate(test_positions):
156
- render_obs = env.render_on_pos(pos)
157
-
158
- # Convert CHW to HWC for PIL
159
- if len(render_obs.shape) == 3 and render_obs.shape[0] == 3:
160
- render_obs = np.transpose(render_obs, (1, 2, 0))
161
-
162
- Image.fromarray(render_obs).save(f"{output_dir}/render_on_pos_{i + 1}.png")
163
-
164
- env.close()
165
- return output_dir
166
-
167
-
168
- def main():
169
- parser = argparse.ArgumentParser(
170
- description="Generate observations for Nine Rooms environment variants"
171
- )
172
- parser.add_argument(
173
- "variant",
174
- choices=["NineRooms", "SpiralNineRooms", "TwentyFiveRooms"],
175
- help="Environment variant to use",
176
- )
177
- parser.add_argument(
178
- "--output-dir",
179
- type=str,
180
- default=None,
181
- help="Output directory for images (default: {variant}_observations)",
182
- )
183
- parser.add_argument(
184
- "--high-res-full",
185
- action="store_true",
186
- help="Generate 512x512 high-resolution full environment views",
187
- )
188
-
189
- args = parser.parse_args()
190
-
191
- output_dir = generate_observations(
192
- args.variant, args.output_dir, args.high_res_full
193
- )
194
-
195
- return output_dir
196
-
197
-
198
- if __name__ == "__main__":
199
- main()
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- generate-observations = miniworld_maze.tools.generate_observations:main
3
-