Spatial-Gym 0.1.1__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.
@@ -0,0 +1,12 @@
1
+ """
2
+ This module initializes the Spatial-Gym environment package.
3
+
4
+ It imports the `Spatial_Gym` class and registers the environment for use with Gymnasium.
5
+
6
+ Modules:
7
+ - Spatial_Gym: Contains the implementation of the Spatial-Gym environment.
8
+ - register_env: Handles the registration of the Spatial-Gym environment with Gymnasium.
9
+ """
10
+
11
+ from .Spatial_Gym import Spatial_Gym
12
+ from .register_env import *
@@ -0,0 +1,8 @@
1
+ from gymnasium.envs.registration import register
2
+ '''
3
+ This script registers the custom environment "Spatial-Gym" with Gymnasium.
4
+ '''
5
+ register(
6
+ id="Spatial-Gym", # Unique identifier for your environment
7
+ entry_point="Spatial_Gym.Spatial_Gym:Spatial_Gym", # Path to your environment class
8
+ )
@@ -0,0 +1,4 @@
1
+ from .human_renderer import HumanRenderer
2
+ from .llm_renderer import LLMRenderer
3
+
4
+ __all__ = ['HumanRenderer', 'LLMRenderer']
@@ -0,0 +1,451 @@
1
+ import pygame
2
+ import math
3
+
4
+
5
+ def draw_line_round_corners_polygon(surf, p1, p2, c, w):
6
+ p1v = pygame.math.Vector2(p1)
7
+ p2v = pygame.math.Vector2(p2)
8
+ lv = (p2v - p1v).normalize()
9
+ lnv = pygame.math.Vector2(-lv.y, lv.x) * w // 2
10
+ pts = [p1v + lnv, p2v + lnv, p2v - lnv, p1v - lnv]
11
+ pygame.draw.polygon(surf, c, pts)
12
+ pygame.draw.circle(surf, c, p1, round(w / 2))
13
+ pygame.draw.circle(surf, c, p2, round(w / 2))
14
+
15
+
16
+ class HumanRenderer:
17
+ """
18
+ Simple renderer for human-friendly visualization with custom colors.
19
+ """
20
+
21
+ def __init__(self, scale_factor=1.0):
22
+ self.screen = None
23
+ self.clock = None
24
+ self.initialized = False
25
+ self.current_cells_x = None
26
+ self.current_cells_y = None
27
+ self.scale_factor = scale_factor
28
+
29
+ # Custom color scheme
30
+ self.GRID_BACKGROUND = (0, 170, 136) # #00AA88 - Teal for grid cells
31
+ self.OVERALL_BACKGROUND = (17, 56, 51) # #113833 - Dark green for overall background
32
+ self.LINE_COLOR = (51, 68, 68) # #334444 - Dark gray-blue for lines
33
+
34
+ def initialize(self, cells_x, cells_y):
35
+ """Initialize pygame and create the display window."""
36
+ if not self.initialized:
37
+ pygame.init()
38
+ # Calculate window size: cells + 20px padding on each side (scaled)
39
+ cell_size = int(40 * self.scale_factor)
40
+ padding = int(20 * self.scale_factor)
41
+ window_width = cells_x * cell_size + 2 * padding # cells + padding
42
+ window_height = cells_y * cell_size + 2 * padding # cells + padding
43
+ self.screen = pygame.display.set_mode((window_width, window_height))
44
+ pygame.display.set_caption("SPaRC Environment - Human View")
45
+ self.clock = pygame.time.Clock()
46
+ self.initialized = True
47
+
48
+ def close(self):
49
+ """Close the pygame display."""
50
+ if self.initialized:
51
+ pygame.display.quit()
52
+ self.initialized = False
53
+
54
+ def render(self, obs_array, color_array, additional_info, polyshapes, x_size, y_size, start_location, target_location, path):
55
+ """
56
+ Renders a simple grid with the specified colors.
57
+
58
+ Parameters:
59
+ obs_array (dict): Dictionary of observation arrays
60
+ color_array (numpy.ndarray): Array containing color information
61
+ additional_info (numpy.ndarray): Array containing additional property info
62
+ polyshapes (dict): Dictionary of polyshape definitions
63
+ x_size (int): Width of the full grid
64
+ y_size (int): Height of the full grid
65
+
66
+ Returns:
67
+ None
68
+ """
69
+ # Calculate actual puzzle cells (excluding gaps)
70
+ cells_x = (x_size - 1) // 2
71
+ cells_y = (y_size - 1) // 2
72
+
73
+ # Check if grid size has changed and reinitialize if needed
74
+ if not self.initialized or self.current_cells_x != cells_x or self.current_cells_y != cells_y:
75
+ if self.initialized:
76
+ # Close existing window before creating new one
77
+ pygame.display.quit()
78
+ self.initialized = False
79
+ self.initialize(cells_x, cells_y)
80
+ self.current_cells_x = cells_x
81
+ self.current_cells_y = cells_y
82
+
83
+ cell_size = int(40 * self.scale_factor)
84
+ padding = int(20 * self.scale_factor)
85
+
86
+ # Fill with overall background color
87
+ self.screen.fill(self.OVERALL_BACKGROUND)
88
+
89
+ # Draw cells with no gaps (adjacent to each other)
90
+ for grid_y in range(cells_y):
91
+ for grid_x in range(cells_x):
92
+ # Calculate position with padding only
93
+ x_pos = padding + grid_x * cell_size
94
+ y_pos = padding + grid_y * cell_size
95
+
96
+ # Draw the cell
97
+ rect = pygame.Rect(x_pos, y_pos, cell_size, cell_size)
98
+ pygame.draw.rect(self.screen, self.GRID_BACKGROUND, rect)
99
+
100
+ # Draw complete grid using the round corner line function
101
+ line_width = int(12 * self.scale_factor)
102
+ gap_size = int(line_width * 1.5) # Size of the gap
103
+
104
+ # Calculate grid boundaries with simple integer coordinates
105
+ grid_left = padding
106
+ grid_right = padding + cells_x * cell_size
107
+ grid_top = padding
108
+ grid_bottom = padding + cells_y * cell_size
109
+
110
+ # Draw all vertical lines with gaps where needed
111
+ for line_x in range(cells_x + 1):
112
+ x_pos = padding + line_x * cell_size
113
+ self._draw_line_with_gaps(
114
+ (x_pos, grid_top), (x_pos, grid_bottom),
115
+ line_x * 2, True, obs_array, line_width, gap_size
116
+ )
117
+
118
+ # Draw all horizontal lines with gaps where needed
119
+ for line_y in range(cells_y + 1):
120
+ y_pos = padding + line_y * cell_size
121
+ self._draw_line_with_gaps(
122
+ (grid_left, y_pos), (grid_right, y_pos),
123
+ line_y * 2, False, obs_array, line_width, gap_size
124
+ )
125
+
126
+ # Use the provided start and target locations
127
+ start_line_x, start_line_y = start_location[0], start_location[1]
128
+ end_line_x, end_line_y = target_location[0], target_location[1]
129
+
130
+ # Find agent location from environment data
131
+ agent_line_x, agent_line_y = None, None
132
+ for y in range(y_size):
133
+ for x in range(x_size):
134
+ if "agent_location" in obs_array and obs_array["agent_location"][x, y] == 1:
135
+ agent_line_x, agent_line_y = x, y
136
+ break
137
+
138
+ # Draw a large circle at the start position
139
+ start_pixel_x = padding + start_line_x * cell_size//2
140
+ start_pixel_y = padding + start_line_y * cell_size//2
141
+ start_circle_radius = int(cell_size // 4)
142
+
143
+ # Check if start position has been visited (has white path through it) and choose circle color accordingly
144
+ start_is_visited = False
145
+ if "visited" in obs_array and start_line_x is not None and start_line_y is not None:
146
+ if (0 <= start_line_y < y_size and 0 <= start_line_x < x_size and
147
+ obs_array["visited"][start_line_x, start_line_y] == 1):
148
+ start_is_visited = True
149
+
150
+ if start_is_visited:
151
+ # Start position has been visited (white path goes through it), draw in white
152
+ start_circle_color = (255, 255, 255)
153
+ else:
154
+ # Start position hasn't been visited, draw in line color
155
+ start_circle_color = self.LINE_COLOR
156
+
157
+ pygame.draw.circle(self.screen, start_circle_color, (start_pixel_x, start_pixel_y), start_circle_radius)
158
+
159
+ # Draw half-line at end location facing outwards
160
+ end_pixel_x = padding + end_line_x * cell_size//2
161
+ end_pixel_y = padding + end_line_y * cell_size//2
162
+
163
+ # Determine which edge the end position is on and face outward
164
+ line_length = int(cell_size // 4)
165
+
166
+ # Check which edge we're on and set line direction accordingly
167
+ if end_line_x == 0: # Left edge
168
+ line_start = (end_pixel_x, end_pixel_y)
169
+ line_end = (end_pixel_x - line_length, end_pixel_y) # Face left
170
+ elif end_line_x == cells_x * 2: # Right edge
171
+ line_start = (end_pixel_x, end_pixel_y)
172
+ line_end = (end_pixel_x + line_length, end_pixel_y) # Face right
173
+ elif end_line_y == 0: # Top edge
174
+ line_start = (end_pixel_x, end_pixel_y)
175
+ line_end = (end_pixel_x, end_pixel_y - line_length) # Face up
176
+ elif end_line_y == cells_y * 2: # Bottom edge
177
+ line_start = (end_pixel_x, end_pixel_y)
178
+ line_end = (end_pixel_x, end_pixel_y + line_length) # Face down
179
+ else: # Default case (shouldn't happen if positioned on edge)
180
+ line_start = (end_pixel_x, end_pixel_y)
181
+ line_end = (end_pixel_x + line_length, end_pixel_y)
182
+
183
+ # Check if end position has been visited (reached by agent) and choose line color accordingly
184
+ end_is_visited = False
185
+ if "visited" in obs_array and end_line_x is not None and end_line_y is not None:
186
+ if (0 <= end_line_y < y_size and 0 <= end_line_x < x_size and
187
+ obs_array["visited"][end_line_x, end_line_y] == 1):
188
+ end_is_visited = True
189
+
190
+ if end_is_visited:
191
+ # End position has been visited (agent reached it), draw in white (agent color)
192
+ end_line_color = (255, 255, 255)
193
+ else:
194
+ # End position hasn't been visited, draw in default line color
195
+ end_line_color = self.LINE_COLOR
196
+
197
+ draw_line_round_corners_polygon(self.screen, line_start, line_end, end_line_color, line_width)
198
+
199
+ # Draw agent path in white
200
+ self._draw_agent_path(path, y_size, x_size, padding, cell_size, line_width)
201
+
202
+ # Draw other properties (symbols)
203
+ for prop, array in obs_array.items():
204
+ if prop in ["visited", "gaps", "agent_location", "target_location"]:
205
+ continue # Skip already visualized properties
206
+
207
+ for y in range(y_size):
208
+ for x in range(x_size):
209
+ if array[x, y]: # If the property exists at this cell
210
+ pixel_x = padding + x * cell_size//2
211
+ pixel_y = padding + y * cell_size//2
212
+ center = (pixel_x, pixel_y)
213
+
214
+ prop_type = prop # e.g., "star", "poly", "triangle", "dot"
215
+ color = color_array[x, y] # Get the color from the color array
216
+ color = self._get_color_from_name(color) # Extract color
217
+
218
+ # Draw the property based on its type
219
+ # Draw a star
220
+ if prop_type == "star":
221
+ self._draw_star(self.screen, color, center, int(cell_size // 6))
222
+
223
+ # Draw a polyshape
224
+ elif prop_type == "poly":
225
+ shape = f'{additional_info[x, y] }' # Get the polyshape name from additional_info
226
+ shape_array = polyshapes[shape]
227
+ top_left = (pixel_x - cell_size//4, pixel_y - cell_size//4)
228
+ self._draw_polyshape(self.screen, shape_array, top_left, cell_size//2, color)
229
+
230
+ # Draw a polyshape with "ylop" type
231
+ elif prop_type == "ylop":
232
+ shape = f'{additional_info[x, y]}' # Get the polyshape name from additional_info
233
+ shape_array = polyshapes[shape]
234
+ top_left = (pixel_x - cell_size//4, pixel_y - cell_size//4)
235
+ self._draw_polyshape(self.screen, shape_array, top_left, cell_size//2, color)
236
+
237
+ # Draw a triangle with a count
238
+ elif prop_type == "triangle":
239
+ triangle_size = int(cell_size // 6)
240
+ pygame.draw.polygon(self.screen, color, [
241
+ (center[0], center[1] - triangle_size), # Top
242
+ (center[0] - triangle_size, center[1] + triangle_size), # Bottom-left
243
+ (center[0] + triangle_size, center[1] + triangle_size) # Bottom-right
244
+ ])
245
+
246
+ # Add text for triangle count
247
+ count = f'{additional_info[x, y]}' # Get the count from additional_info
248
+ font = pygame.font.Font(None, int(16 * self.scale_factor))
249
+ text = font.render(count, True, (255, 255, 255))
250
+ shadow = font.render(count, True, (0, 0, 0))
251
+ shadow_pos = (center[0] - int(4 * self.scale_factor) + 1, center[1] - int(12 * self.scale_factor) + 1)
252
+ self.screen.blit(shadow, shadow_pos)
253
+ text_pos = (center[0] - int(4 * self.scale_factor), center[1] - int(12 * self.scale_factor))
254
+ self.screen.blit(text, text_pos)
255
+
256
+ # Draw a square
257
+ elif prop_type == "square":
258
+ square_size = int(cell_size // 4)
259
+ square_rect = pygame.Rect(
260
+ center[0] - square_size // 2,
261
+ center[1] - square_size // 2,
262
+ square_size,
263
+ square_size
264
+ )
265
+ pygame.draw.rect(self.screen, color, square_rect)
266
+
267
+ # Draw a dot as black hexagon
268
+ elif prop_type == "dot":
269
+ self._draw_hexagon(pixel_x, pixel_y, int(line_width // 3), (0, 0, 0))
270
+
271
+ pygame.display.flip()
272
+ self.clock.tick(30) # Limit to 30 FPS
273
+
274
+ # Handle pygame events
275
+ for event in pygame.event.get():
276
+ if event.type == pygame.QUIT:
277
+ pygame.quit()
278
+ exit()
279
+
280
+ def _draw_hexagon(self, center_x, center_y, radius, color):
281
+ """Draw a hexagon at the specified center position."""
282
+ points = []
283
+ for i in range(6):
284
+ angle = i * 60 * math.pi / 180 # Convert degrees to radians
285
+ x = center_x + radius * math.cos(angle)
286
+ y = center_y + radius * math.sin(angle)
287
+ points.append((x, y))
288
+ pygame.draw.polygon(self.screen, color, points)
289
+
290
+ def _draw_star(self, surface, color, center, radius):
291
+ """
292
+ Draws a star shape at a specified location.
293
+
294
+ Parameters:
295
+ surface (pygame.Surface): The Pygame surface to draw on.
296
+ color (tuple): The RGB color to use for the star.
297
+ center (tuple): The (x, y) coordinates of the center of the star.
298
+ radius (int): The radius of the star, determining its size.
299
+ """
300
+ points = []
301
+ for i in range(10):
302
+ angle = math.pi / 2 + i * math.pi / 5
303
+ r = radius if i % 2 == 0 else radius // 2
304
+ x = center[0] + int(math.cos(angle) * r)
305
+ y = center[1] - int(math.sin(angle) * r)
306
+ points.append((x, y))
307
+ pygame.draw.polygon(surface, color, points)
308
+
309
+ def _draw_polyshape(self, surface, shape_array, top_left, cell_size, color):
310
+ """
311
+ Draws a polyshape inside a given cell.
312
+
313
+ Parameters:
314
+ surface (pygame.Surface): The Pygame surface to draw on.
315
+ shape_array (list of lists): A 2D array representing the polyshape, where 1 indicates a filled cell and 0 indicates an empty cell.
316
+ top_left (tuple): The top-left corner of the cell
317
+ cell_size (int): The size of the cell in pixels.
318
+ color (tuple): The RGB color to use for the polyshape.
319
+ """
320
+
321
+ shape_height = len(shape_array)
322
+ shape_width = len(shape_array[0])
323
+
324
+ padding = cell_size // 6
325
+
326
+ mini_block_width = (cell_size - 2 * padding) // shape_width
327
+ mini_block_height = (cell_size - 2 * padding) // shape_height
328
+
329
+ for y, row in enumerate(shape_array):
330
+ for x, val in enumerate(row):
331
+ if val:
332
+ rect = pygame.Rect(
333
+ top_left[0] + padding + x * mini_block_width,
334
+ top_left[1] + padding + y * mini_block_height,
335
+ mini_block_width,
336
+ mini_block_height
337
+ )
338
+ pygame.draw.rect(surface, color, rect)
339
+
340
+ def _get_color_from_name(self, color):
341
+ """
342
+ Helper function to extract color from property name parts.
343
+ """
344
+ if color == 1:
345
+ return (255, 0, 0) # Red
346
+ elif color == 2:
347
+ return (0, 0, 255) # Blue
348
+ elif color == 3:
349
+ return (255, 255, 0) # Yellow
350
+ elif color == 4:
351
+ return (0, 255, 0) # Green
352
+ elif color == 5:
353
+ return (0, 0, 0) # Black
354
+ elif color == 6:
355
+ return (128, 0, 128) # Purple
356
+ elif color == 7:
357
+ return (255, 165, 0) # Orange
358
+ elif color == 8:
359
+ return (255, 255, 255) # White
360
+ else:
361
+ return (128, 128, 128) # Default: Gray
362
+
363
+ def _draw_agent_path(self, path, y_size, x_size, padding, cell_size, line_width):
364
+ """Draw the agent's path as white lines connecting positions in order."""
365
+ if not path or len(path) < 2:
366
+ return
367
+
368
+ # Draw white line segments connecting consecutive positions in the path
369
+ white_line_width = max(1, int(line_width))
370
+
371
+ for i in range(len(path) - 1):
372
+ current_pos = path[i] # [x, y] format
373
+ next_pos = path[i + 1] # [x, y] format
374
+
375
+ # Convert to pixel coordinates (note the coordinate swap: path uses [x,y] but pixel calc needs [y,x])
376
+ current_pixel = (
377
+ padding + current_pos[0] * cell_size // 2,
378
+ padding + current_pos[1] * cell_size // 2
379
+ )
380
+ next_pixel = (
381
+ padding + next_pos[0] * cell_size // 2,
382
+ padding + next_pos[1] * cell_size // 2
383
+ )
384
+
385
+ # Draw white line segment between consecutive path positions
386
+ draw_line_round_corners_polygon(
387
+ self.screen, current_pixel, next_pixel,
388
+ (255, 255, 255), white_line_width
389
+ )
390
+
391
+ def _draw_line_with_gaps(self, start_pos, end_pos, line_coord, is_vertical, obs_array, line_width, gap_size):
392
+ """Draw a line with gaps where specified in the obs_array."""
393
+ cell_size = int(40 * self.scale_factor)
394
+ padding = int(20 * self.scale_factor)
395
+
396
+ # Find all gap positions along this line
397
+ gap_positions = []
398
+ if "gaps" in obs_array:
399
+ if is_vertical:
400
+ # For vertical lines, check all y positions at this x coordinate
401
+ for y in range(obs_array["gaps"].shape[1]):
402
+ if obs_array["gaps"][line_coord, y] == 1:
403
+ gap_pixel_y = padding + y * cell_size // 2
404
+ gap_positions.append(gap_pixel_y)
405
+ else:
406
+ # For horizontal lines, check all x positions at this y coordinate
407
+ for x in range(obs_array["gaps"].shape[0]):
408
+ if obs_array["gaps"][x, line_coord] == 1:
409
+ gap_pixel_x = padding + x * cell_size // 2
410
+ gap_positions.append(gap_pixel_x)
411
+
412
+ # If no gaps, draw the complete line
413
+ if not gap_positions:
414
+ draw_line_round_corners_polygon(self.screen, start_pos, end_pos, self.LINE_COLOR, line_width)
415
+ return
416
+
417
+ # Draw line segments around gaps using polygon line function
418
+ if is_vertical:
419
+ current_y = start_pos[1]
420
+ for gap_y in sorted(gap_positions):
421
+ # Draw segment before gap
422
+ if current_y < gap_y - gap_size // 2:
423
+ segment_start = (start_pos[0], current_y)
424
+ segment_end = (start_pos[0], gap_y - gap_size // 2)
425
+ draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)
426
+
427
+ # Skip over the gap
428
+ current_y = gap_y + gap_size // 2
429
+
430
+ # Draw final segment after last gap
431
+ if current_y < end_pos[1]:
432
+ segment_start = (start_pos[0], current_y)
433
+ segment_end = end_pos
434
+ draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)
435
+ else:
436
+ current_x = start_pos[0]
437
+ for gap_x in sorted(gap_positions):
438
+ # Draw segment before gap
439
+ if current_x < gap_x - gap_size // 2:
440
+ segment_start = (current_x, start_pos[1])
441
+ segment_end = (gap_x - gap_size // 2, start_pos[1])
442
+ draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)
443
+
444
+ # Skip over the gap
445
+ current_x = gap_x + gap_size // 2
446
+
447
+ # Draw final segment after last gap
448
+ if current_x < end_pos[0]:
449
+ segment_start = (current_x, start_pos[1])
450
+ segment_end = end_pos
451
+ draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)