1+ import numpy as np
2+ import gym
3+ from griddly .util .rllib .environment .level_generator import LevelGenerator
4+
5+ class LabyrinthLevelGenerator (LevelGenerator ):
6+ WALL = 'w'
7+ AGENT = 'A'
8+ GOAL = 'x'
9+ EMPTY = '.'
10+
11+ def __init__ (self , config ):
12+ """
13+ Initialize the LabyrinthLevelGenerator.
14+
15+ Parameters:
16+ config (dict): Configuration dictionary with the following optional keys:
17+ - width (int): Width of the maze (default: 9).
18+ - height (int): Height of the maze (default: 9).
19+ - wall_density (float): Wall density in the maze (default: 1).
20+ - num_goals (int): Number of goals to place in the maze (default: 1).
21+
22+ Example usage:
23+ --------------
24+ config = {
25+ 'width': 9,
26+ 'height': 9,
27+ 'wall_density': 1,
28+ 'num_goals': 1
29+ }
30+ level_generator = LabyrinthLevelGenerator(config)
31+ level_string = level_generator.generate()
32+ env = gym.make('GDY-Maze-v0')
33+ env.reset(level_string=level_string)
34+ """
35+
36+ super ().__init__ (config )
37+ self ._width = config .get ('width' , 9 )
38+ self ._height = config .get ('height' , 9 )
39+ self ._wall_density = config .get ('wall_density' , 1 ) # Adjust this value to control wall density
40+
41+ assert self ._width % 2 == 1 and self ._height % 2 == 1
42+
43+ self ._num_goals = config .get ('num_goals' , 1 )
44+
45+ def _generate_maze (self ):
46+ """
47+ Generate the maze grid.
48+
49+ Returns:
50+ np.ndarray: 2D array representing the maze with walls, empty spaces, and paths.
51+
52+ Note: The Recursive Backtracking algorithm is used to generate the paths in the maze.
53+ """
54+
55+ # Create a maze grid with walls
56+ maze = np .full ((self ._width , self ._height ), LabyrinthLevelGenerator .WALL , dtype = np .dtype ('U1' ))
57+
58+ # Recursive Backtracking algorithm for generating maze paths
59+ def recursive_backtracking (x , y ):
60+ maze [x , y ] = LabyrinthLevelGenerator .EMPTY
61+
62+ # Randomize the order of directions to explore
63+ directions = [(0 , 1 ), (1 , 0 ), (0 , - 1 ), (- 1 , 0 )]
64+ np .random .shuffle (directions )
65+
66+ for dx , dy in directions :
67+ nx , ny = x + 2 * dx , y + 2 * dy
68+ if 0 <= nx < self ._width and 0 <= ny < self ._height and maze [nx , ny ] == LabyrinthLevelGenerator .WALL :
69+ # Carve a path by removing walls
70+ maze [nx - dx , ny - dy ] = LabyrinthLevelGenerator .EMPTY
71+ recursive_backtracking (nx , ny )
72+
73+ # Start the Recursive Backtracking from a random position
74+ start_x , start_y = np .random .choice (range (1 , self ._width - 1 ), size = 2 ), np .random .choice (range (1 , self ._height - 1 ), size = 2 )
75+ recursive_backtracking (start_x [0 ], start_y [0 ])
76+
77+ # Add more open spaces by removing walls randomly
78+ for x in range (1 , self ._width - 1 ):
79+ for y in range (1 , self ._height - 1 ):
80+ if maze [x , y ] == LabyrinthLevelGenerator .WALL and np .random .random () > self ._wall_density :
81+ maze [x , y ] = LabyrinthLevelGenerator .EMPTY
82+
83+ return maze
84+
85+ def _is_reachable (self , maze , x , y ):
86+ """
87+ Check if a tile is reachable from the agent's starting position using a flood-fill algorithm.
88+
89+ Parameters:
90+ maze (np.ndarray): 2D array representing the maze grid.
91+ x (int): X-coordinate of the tile to check.
92+ y (int): Y-coordinate of the tile to check.
93+
94+ Returns:
95+ bool: True if the tile is reachable; False otherwise.
96+ """
97+
98+ # Flood-fill algorithm to check if (x, y) is reachable from the agent's starting position
99+ stack = [(x , y )]
100+ visited = set ()
101+
102+ while stack :
103+ cx , cy = stack .pop ()
104+ if (cx , cy ) in visited :
105+ continue
106+
107+ visited .add ((cx , cy ))
108+ for dx , dy in [(1 , 0 ), (- 1 , 0 ), (0 , 1 ), (0 , - 1 )]:
109+ nx , ny = cx + dx , cy + dy
110+ if (
111+ 0 <= nx < self ._width
112+ and 0 <= ny < self ._height
113+ and maze [nx , ny ] != LabyrinthLevelGenerator .WALL
114+ and (nx , ny ) != (x , y ) # Exclude the goal position from the flood-fill
115+ ):
116+ stack .append ((nx , ny ))
117+
118+ return len (visited ) == self ._width * self ._height - np .sum (maze == LabyrinthLevelGenerator .WALL )
119+
120+ def _place_goals (self , maze , agent_x , agent_y ):
121+ """
122+ Place the goals in the maze while ensuring they don't block the agent's access to all tiles.
123+
124+ Parameters:
125+ maze (np.ndarray): 2D array representing the maze grid.
126+ agent_x (int): X-coordinate of the agent's starting position.
127+ agent_y (int): Y-coordinate of the agent's starting position.
128+
129+ Returns:
130+ np.ndarray: Updated maze grid with goals placed.
131+
132+ Note: The goals are placed in locations that do not block the agent's navigation to all tiles.
133+ """
134+
135+ # Get all available empty spaces for goal placement
136+ available_spaces = np .transpose (np .where (maze == LabyrinthLevelGenerator .EMPTY ))
137+
138+ for _ in range (self ._num_goals ):
139+ np .random .shuffle (available_spaces )
140+ for goal_x , goal_y in available_spaces :
141+ # Check if the goal location does not block the agent's access to all tiles of the maze
142+ maze [goal_x , goal_y ] = LabyrinthLevelGenerator .GOAL
143+ if self ._is_reachable (maze , agent_x , agent_y ):
144+ break
145+ else :
146+ maze [goal_x , goal_y ] = LabyrinthLevelGenerator .EMPTY
147+
148+ return maze
149+
150+ def generate (self ):
151+ """
152+ Generate a new maze level.
153+
154+ Returns:
155+ str: String representation of the maze level.
156+
157+ Example usage:
158+ --------------
159+ config = {
160+ 'width': 9,
161+ 'height': 9,
162+ 'wall_density': 1,
163+ 'num_goals': 1
164+ }
165+ level_generator = LabyrinthLevelGenerator(config)
166+ level_string = level_generator.generate()
167+ env = gym.make('GDY-Maze-v0')
168+ env.reset(level_string=level_string)
169+ """
170+
171+ maze = self ._generate_maze ()
172+
173+ # Place agent
174+ agent_x = 2 * np .random .randint (1 , (self ._width - 1 ) // 2 )
175+ agent_y = 2 * np .random .randint (1 , (self ._height - 1 ) // 2 )
176+ maze [agent_x , agent_y ] = LabyrinthLevelGenerator .AGENT
177+
178+ # Place goals with minimum distance constraint
179+ maze = self ._place_goals (maze , agent_x , agent_y )
180+
181+ level_string = '\n ' .join (['' .join (row ) for row in maze ])
182+
183+ return level_string
184+
185+ if __name__ == '__main__' :
186+ import matplotlib .pyplot as plt
187+
188+ env = gym .make ('GDY-Labyrinth-v0' )
189+ sizes = ["45x45" , "21x21" , "13x13" ]
190+
191+ for size in sizes :
192+ for i in range (3 ):
193+ config = {
194+ 'width' : int (size .split ('x' )[0 ]),
195+ 'height' : int (size .split ('x' )[1 ]),
196+ }
197+
198+ level_generator = LabyrinthLevelGenerator (config )
199+ env .reset (level_string = level_generator .generate ())
200+
201+ obs = env .render (mode = "rgb_array" )
202+ plt .figure ()
203+ plt .imshow (obs )
204+ plt .savefig (f"example_maze_{ size } _{ i + 1 } .png" )
0 commit comments