synth-ai 0.2.4.dev4__py3-none-any.whl → 0.2.4.dev6__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.
Files changed (123) hide show
  1. synth_ai/environments/examples/__init__.py +1 -0
  2. synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
  3. synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
  4. synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
  5. synth_ai/environments/examples/crafter_classic/engine.py +579 -0
  6. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
  7. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
  8. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
  9. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +266 -0
  10. synth_ai/environments/examples/crafter_classic/environment.py +364 -0
  11. synth_ai/environments/examples/crafter_classic/taskset.py +233 -0
  12. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +229 -0
  13. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +298 -0
  14. synth_ai/environments/examples/crafter_custom/__init__.py +4 -0
  15. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +7 -0
  16. synth_ai/environments/examples/crafter_custom/crafter/config.py +182 -0
  17. synth_ai/environments/examples/crafter_custom/crafter/constants.py +8 -0
  18. synth_ai/environments/examples/crafter_custom/crafter/engine.py +269 -0
  19. synth_ai/environments/examples/crafter_custom/crafter/env.py +266 -0
  20. synth_ai/environments/examples/crafter_custom/crafter/objects.py +418 -0
  21. synth_ai/environments/examples/crafter_custom/crafter/recorder.py +187 -0
  22. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +119 -0
  23. synth_ai/environments/examples/crafter_custom/dataset_builder.py +373 -0
  24. synth_ai/environments/examples/crafter_custom/environment.py +312 -0
  25. synth_ai/environments/examples/crafter_custom/run_dataset.py +305 -0
  26. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
  27. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
  28. synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
  29. synth_ai/environments/examples/enron/engine.py +291 -0
  30. synth_ai/environments/examples/enron/environment.py +165 -0
  31. synth_ai/environments/examples/enron/taskset.py +112 -0
  32. synth_ai/environments/examples/minigrid/__init__.py +48 -0
  33. synth_ai/environments/examples/minigrid/engine.py +589 -0
  34. synth_ai/environments/examples/minigrid/environment.py +274 -0
  35. synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
  36. synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
  37. synth_ai/environments/examples/minigrid/taskset.py +583 -0
  38. synth_ai/environments/examples/nethack/__init__.py +7 -0
  39. synth_ai/environments/examples/nethack/achievements.py +337 -0
  40. synth_ai/environments/examples/nethack/engine.py +738 -0
  41. synth_ai/environments/examples/nethack/environment.py +255 -0
  42. synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
  43. synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
  44. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
  45. synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
  46. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
  47. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
  48. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
  49. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
  50. synth_ai/environments/examples/nethack/taskset.py +323 -0
  51. synth_ai/environments/examples/red/__init__.py +7 -0
  52. synth_ai/environments/examples/red/config_logging.py +110 -0
  53. synth_ai/environments/examples/red/engine.py +693 -0
  54. synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
  55. synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
  56. synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
  57. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
  58. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
  59. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
  60. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
  61. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
  62. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
  63. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
  64. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
  65. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
  66. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
  67. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
  68. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
  69. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
  70. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
  71. synth_ai/environments/examples/red/environment.py +235 -0
  72. synth_ai/environments/examples/red/taskset.py +77 -0
  73. synth_ai/environments/examples/sokoban/__init__.py +1 -0
  74. synth_ai/environments/examples/sokoban/engine.py +675 -0
  75. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
  76. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
  77. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
  78. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
  79. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
  80. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
  81. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
  82. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
  83. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
  84. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
  85. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
  86. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
  87. synth_ai/environments/examples/sokoban/environment.py +228 -0
  88. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
  89. synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
  90. synth_ai/environments/examples/sokoban/taskset.py +425 -0
  91. synth_ai/environments/examples/tictactoe/__init__.py +1 -0
  92. synth_ai/environments/examples/tictactoe/engine.py +368 -0
  93. synth_ai/environments/examples/tictactoe/environment.py +239 -0
  94. synth_ai/environments/examples/tictactoe/taskset.py +214 -0
  95. synth_ai/environments/examples/verilog/__init__.py +10 -0
  96. synth_ai/environments/examples/verilog/engine.py +328 -0
  97. synth_ai/environments/examples/verilog/environment.py +349 -0
  98. synth_ai/environments/examples/verilog/taskset.py +418 -0
  99. synth_ai/environments/examples/wordle/__init__.py +29 -0
  100. synth_ai/environments/examples/wordle/engine.py +391 -0
  101. synth_ai/environments/examples/wordle/environment.py +154 -0
  102. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +75 -0
  103. synth_ai/environments/examples/wordle/taskset.py +222 -0
  104. synth_ai/environments/service/app.py +8 -0
  105. synth_ai/environments/service/core_routes.py +38 -0
  106. synth_ai/learning/prompts/banking77_injection_eval.py +163 -0
  107. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +201 -0
  108. synth_ai/learning/prompts/mipro.py +273 -1
  109. synth_ai/learning/prompts/random_search.py +247 -0
  110. synth_ai/learning/prompts/run_mipro_banking77.py +160 -0
  111. synth_ai/learning/prompts/run_random_search_banking77.py +305 -0
  112. synth_ai/lm/injection.py +81 -0
  113. synth_ai/lm/overrides.py +204 -0
  114. synth_ai/lm/provider_support/anthropic.py +39 -12
  115. synth_ai/lm/provider_support/openai.py +31 -4
  116. synth_ai/lm/vendors/core/anthropic_api.py +16 -0
  117. synth_ai/lm/vendors/openai_standard.py +35 -5
  118. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/METADATA +2 -1
  119. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/RECORD +123 -13
  120. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/WHEEL +0 -0
  121. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/entry_points.txt +0 -0
  122. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/licenses/LICENSE +0 -0
  123. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,416 @@
1
+ """
2
+ MiniGrid Puzzle Loader
3
+
4
+ This module provides a comprehensive puzzle loading system for MiniGrid environments
5
+ with deterministic seed-based selection and difficulty filtering.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass, asdict
10
+ from typing import Dict, List, Optional, Tuple, Any
11
+ from synth_ai.environments.examples.minigrid.environment_mapping import ENVIRONMENT_MAPPING
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class MiniGridPuzzle:
18
+ """Represents a single MiniGrid puzzle configuration."""
19
+
20
+ id: str
21
+ environment_name: str
22
+ difficulty: str
23
+ seed: int
24
+ grid_size: Tuple[int, int]
25
+ mission_description: str
26
+
27
+ # Environment features
28
+ has_key: bool = False
29
+ has_door: bool = False
30
+ has_lava: bool = False
31
+ has_multi_room: bool = False
32
+ num_objects: int = 0
33
+
34
+ # Difficulty metrics
35
+ complexity_score: float = 0.0
36
+ estimated_steps: int = 0
37
+
38
+ def to_dict(self) -> Dict[str, Any]:
39
+ """Convert puzzle to dictionary."""
40
+ return asdict(self)
41
+
42
+ @classmethod
43
+ def from_dict(cls, data: Dict[str, Any]) -> "MiniGridPuzzle":
44
+ """Create puzzle from dictionary."""
45
+ return cls(**data)
46
+
47
+
48
+ class MiniGridPuzzleLoader:
49
+ """Manages loading and accessing MiniGrid puzzles with difficulty filtering."""
50
+
51
+ def __init__(self):
52
+ self.puzzles: Dict[str, List[MiniGridPuzzle]] = {}
53
+ self.all_puzzles: List[MiniGridPuzzle] = []
54
+ self._loaded = False
55
+
56
+ # Difficulty seed mappings based on user's detailed analysis
57
+ self.difficulty_seeds = {
58
+ "ultra_easy": [
59
+ 0,
60
+ 1,
61
+ 2,
62
+ 3,
63
+ 4,
64
+ 5,
65
+ 7,
66
+ 8,
67
+ 9,
68
+ 10,
69
+ 12,
70
+ 13,
71
+ 14,
72
+ 15,
73
+ 16,
74
+ 17,
75
+ 18,
76
+ 19,
77
+ 20,
78
+ 21,
79
+ 22,
80
+ 23,
81
+ ],
82
+ "easy": [11, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34],
83
+ "medium": [35, 36, 37, 38, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49],
84
+ "hard": [6, 42, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
85
+ }
86
+
87
+ def load_puzzles(self) -> None:
88
+ """Load all MiniGrid puzzles from the environment mapping."""
89
+ if self._loaded:
90
+ return
91
+
92
+ logger.info("Loading MiniGrid puzzles from environment mapping...")
93
+
94
+ # Clear existing data
95
+ self.puzzles.clear()
96
+ self.all_puzzles.clear()
97
+
98
+ # Initialize difficulty categories
99
+ for difficulty in self.difficulty_seeds.keys():
100
+ self.puzzles[difficulty] = []
101
+
102
+ # Load puzzles for each difficulty
103
+ for difficulty, seeds in self.difficulty_seeds.items():
104
+ for seed in seeds:
105
+ puzzle = self._create_puzzle_from_seed(seed, difficulty)
106
+ if puzzle:
107
+ self.puzzles[difficulty].append(puzzle)
108
+ self.all_puzzles.append(puzzle)
109
+
110
+ self._loaded = True
111
+ logger.info(
112
+ f"Loaded {len(self.all_puzzles)} MiniGrid puzzles across {len(self.puzzles)} difficulties"
113
+ )
114
+
115
+ def _create_puzzle_from_seed(self, seed: int, difficulty: str) -> Optional[MiniGridPuzzle]:
116
+ """Create a puzzle from a seed and difficulty."""
117
+ if seed not in ENVIRONMENT_MAPPING:
118
+ logger.warning(f"Seed {seed} not found in environment mapping")
119
+ return None
120
+
121
+ env_name = ENVIRONMENT_MAPPING[seed]
122
+ puzzle_id = f"{difficulty}_{seed:03d}"
123
+
124
+ # Extract environment features
125
+ has_key = "DoorKey" in env_name or "Unlock" in env_name or "KeyCorridor" in env_name
126
+ has_door = "Door" in env_name or "Room" in env_name or "Unlock" in env_name
127
+ has_lava = "Lava" in env_name
128
+ has_multi_room = "MultiRoom" in env_name or "FourRooms" in env_name
129
+
130
+ # Estimate grid size from environment name
131
+ grid_size = self._estimate_grid_size(env_name)
132
+
133
+ # Count objects
134
+ num_objects = 0
135
+ if has_key:
136
+ num_objects += 1
137
+ if has_door:
138
+ num_objects += 1
139
+ if "Pickup" in env_name:
140
+ num_objects += 1
141
+ if "Fetch" in env_name:
142
+ if "N2" in env_name:
143
+ num_objects += 2
144
+ elif "N3" in env_name:
145
+ num_objects += 3
146
+
147
+ # Generate mission description
148
+ mission_description = self._generate_mission_description(
149
+ env_name, has_key, has_door, has_lava, has_multi_room
150
+ )
151
+
152
+ # Calculate complexity score
153
+ complexity_score = self._calculate_complexity_score(
154
+ grid_size, num_objects, has_key, has_door, has_lava, has_multi_room
155
+ )
156
+
157
+ # Estimate steps
158
+ estimated_steps = self._estimate_steps(grid_size, complexity_score)
159
+
160
+ return MiniGridPuzzle(
161
+ id=puzzle_id,
162
+ environment_name=env_name,
163
+ difficulty=difficulty,
164
+ seed=seed,
165
+ grid_size=grid_size,
166
+ mission_description=mission_description,
167
+ has_key=has_key,
168
+ has_door=has_door,
169
+ has_lava=has_lava,
170
+ has_multi_room=has_multi_room,
171
+ num_objects=num_objects,
172
+ complexity_score=complexity_score,
173
+ estimated_steps=estimated_steps,
174
+ )
175
+
176
+ def _estimate_grid_size(self, env_name: str) -> Tuple[int, int]:
177
+ """Estimate grid size from environment name."""
178
+ if "5x5" in env_name:
179
+ return (5, 5)
180
+ elif "6x6" in env_name:
181
+ return (6, 6)
182
+ elif "8x8" in env_name:
183
+ return (8, 8)
184
+ elif "16x16" in env_name:
185
+ return (16, 16)
186
+ elif "FourRooms" in env_name:
187
+ return (19, 19)
188
+ elif "MultiRoom-N2" in env_name:
189
+ return (15, 15)
190
+ elif "MultiRoom-N4" in env_name:
191
+ return (19, 19)
192
+ elif "MultiRoom-N6" in env_name:
193
+ return (25, 25)
194
+ elif "LavaGapS5" in env_name:
195
+ return (5, 7)
196
+ elif "LavaGapS6" in env_name:
197
+ return (6, 8)
198
+ elif "LavaGapS7" in env_name:
199
+ return (7, 9)
200
+ elif "CrossingS9" in env_name:
201
+ return (9, 9)
202
+ elif "CrossingS11" in env_name:
203
+ return (11, 11)
204
+ else:
205
+ return (7, 7) # Default
206
+
207
+ def _generate_mission_description(
208
+ self, env_name: str, has_key: bool, has_door: bool, has_lava: bool, has_multi_room: bool
209
+ ) -> str:
210
+ """Generate mission description based on environment features."""
211
+ if "Empty" in env_name:
212
+ return "Navigate the grid to reach the goal"
213
+ elif "DoorKey" in env_name:
214
+ return "Find the key, unlock the door, and reach the goal"
215
+ elif "Unlock" in env_name:
216
+ return "Use keys to unlock doors and reach the goal"
217
+ elif "MultiRoom" in env_name:
218
+ return "Navigate through multiple rooms to reach the goal"
219
+ elif "LavaGap" in env_name:
220
+ return "Jump over lava gaps to reach the goal"
221
+ elif "LavaCrossing" in env_name:
222
+ return "Navigate through lava fields to reach the goal"
223
+ elif "Fetch" in env_name:
224
+ return "Pick up the required objects"
225
+ elif "PutNear" in env_name:
226
+ return "Pick up objects and place them near other objects"
227
+ elif "KeyCorridor" in env_name:
228
+ return "Navigate corridors with keys and doors"
229
+ elif "FourRooms" in env_name:
230
+ return "Navigate through four connected rooms to reach the goal"
231
+ else:
232
+ return "Complete the mission to reach the goal"
233
+
234
+ def _calculate_complexity_score(
235
+ self,
236
+ grid_size: Tuple[int, int],
237
+ num_objects: int,
238
+ has_key: bool,
239
+ has_door: bool,
240
+ has_lava: bool,
241
+ has_multi_room: bool,
242
+ ) -> float:
243
+ """Calculate complexity score based on environment features."""
244
+ width, height = grid_size
245
+ base_score = (width * height) / 100.0 # Normalized by grid size
246
+
247
+ # Add complexity for features
248
+ if has_key:
249
+ base_score += 0.5
250
+ if has_door:
251
+ base_score += 0.3
252
+ if has_lava:
253
+ base_score += 0.7
254
+ if has_multi_room:
255
+ base_score += 1.0
256
+
257
+ # Add complexity for objects
258
+ base_score += num_objects * 0.2
259
+
260
+ return base_score
261
+
262
+ def _estimate_steps(self, grid_size: Tuple[int, int], complexity_score: float) -> int:
263
+ """Estimate number of steps required."""
264
+ width, height = grid_size
265
+ base_steps = width + height # Manhattan distance estimate
266
+
267
+ # Scale by complexity
268
+ estimated = int(base_steps * (1.0 + complexity_score))
269
+
270
+ return max(10, estimated) # Minimum 10 steps
271
+
272
+ def get_puzzles_by_difficulty(self, difficulty: str) -> List[MiniGridPuzzle]:
273
+ """Get all puzzles for a specific difficulty level."""
274
+ if not self._loaded:
275
+ self.load_puzzles()
276
+ return self.puzzles.get(difficulty, [])
277
+
278
+ def get_puzzle_by_seed(self, difficulty: str, seed: int) -> Optional[MiniGridPuzzle]:
279
+ """
280
+ Get a puzzle deterministically using a seed via modular arithmetic.
281
+ Same seed will always return the same puzzle for a given difficulty.
282
+ """
283
+ puzzles = self.get_puzzles_by_difficulty(difficulty)
284
+ if not puzzles:
285
+ return None
286
+
287
+ # Use modular arithmetic to map seed to puzzle index
288
+ index = seed % len(puzzles)
289
+ return puzzles[index]
290
+
291
+ def get_puzzle_by_index(self, difficulty: str, index: int) -> Optional[MiniGridPuzzle]:
292
+ """Get a puzzle by its index within a difficulty level."""
293
+ puzzles = self.get_puzzles_by_difficulty(difficulty)
294
+ if 0 <= index < len(puzzles):
295
+ return puzzles[index]
296
+ return None
297
+
298
+ def get_random_puzzle(self, difficulty: str) -> Optional[MiniGridPuzzle]:
299
+ """Get a random puzzle from the specified difficulty."""
300
+ import random
301
+
302
+ puzzles = self.get_puzzles_by_difficulty(difficulty)
303
+ if not puzzles:
304
+ return None
305
+ return random.choice(puzzles)
306
+
307
+ def get_available_difficulties(self) -> List[str]:
308
+ """Get list of available difficulty levels."""
309
+ return list(self.difficulty_seeds.keys())
310
+
311
+ def get_metadata_for_filtering(self) -> Dict[str, Any]:
312
+ """Get metadata to help with filtering across environments."""
313
+ if not self._loaded:
314
+ self.load_puzzles()
315
+
316
+ return {
317
+ "environment_type": "minigrid",
318
+ "total_puzzles": len(self.all_puzzles),
319
+ "difficulties": self.get_available_difficulties(),
320
+ "difficulty_counts": {
321
+ difficulty: len(puzzles) for difficulty, puzzles in self.puzzles.items()
322
+ },
323
+ "features": {
324
+ "has_navigation": True,
325
+ "has_keys": True,
326
+ "has_doors": True,
327
+ "has_lava": True,
328
+ "has_multi_room": True,
329
+ "grid_based": True,
330
+ "puzzle_type": "navigation",
331
+ },
332
+ "difficulty_ranges": {
333
+ "ultra_easy": {"grid_size": (5, 7), "complexity": (0.0, 1.0), "environments": 22},
334
+ "easy": {"grid_size": (6, 15), "complexity": (1.0, 2.0), "environments": 12},
335
+ "medium": {"grid_size": (7, 21), "complexity": (2.0, 4.0), "environments": 14},
336
+ "hard": {"grid_size": (8, 37), "complexity": (4.0, 8.0), "environments": 12},
337
+ },
338
+ }
339
+
340
+ def get_total_puzzle_count(self) -> int:
341
+ """Get total number of puzzles across all difficulties."""
342
+ if not self._loaded:
343
+ self.load_puzzles()
344
+ return len(self.all_puzzles)
345
+
346
+ def get_difficulty_counts(self) -> Dict[str, int]:
347
+ """Get count of puzzles per difficulty level."""
348
+ if not self._loaded:
349
+ self.load_puzzles()
350
+ return {difficulty: len(puzzles) for difficulty, puzzles in self.puzzles.items()}
351
+
352
+ def filter_puzzles(self, **kwargs) -> List[MiniGridPuzzle]:
353
+ """Filter puzzles by various criteria."""
354
+ if not self._loaded:
355
+ self.load_puzzles()
356
+
357
+ filtered = self.all_puzzles
358
+
359
+ # Filter by difficulty
360
+ if "difficulty" in kwargs:
361
+ filtered = [p for p in filtered if p.difficulty == kwargs["difficulty"]]
362
+
363
+ # Filter by features
364
+ if "has_key" in kwargs:
365
+ filtered = [p for p in filtered if p.has_key == kwargs["has_key"]]
366
+ if "has_door" in kwargs:
367
+ filtered = [p for p in filtered if p.has_door == kwargs["has_door"]]
368
+ if "has_lava" in kwargs:
369
+ filtered = [p for p in filtered if p.has_lava == kwargs["has_lava"]]
370
+ if "has_multi_room" in kwargs:
371
+ filtered = [p for p in filtered if p.has_multi_room == kwargs["has_multi_room"]]
372
+
373
+ # Filter by grid size
374
+ if "max_width" in kwargs:
375
+ filtered = [p for p in filtered if p.grid_size[0] <= kwargs["max_width"]]
376
+ if "max_height" in kwargs:
377
+ filtered = [p for p in filtered if p.grid_size[1] <= kwargs["max_height"]]
378
+
379
+ # Filter by complexity
380
+ if "max_complexity" in kwargs:
381
+ filtered = [p for p in filtered if p.complexity_score <= kwargs["max_complexity"]]
382
+
383
+ return filtered
384
+
385
+
386
+ # Global puzzle loader instance
387
+ _puzzle_loader: Optional[MiniGridPuzzleLoader] = None
388
+
389
+
390
+ def get_puzzle_loader() -> MiniGridPuzzleLoader:
391
+ """Get the global puzzle loader instance."""
392
+ global _puzzle_loader
393
+ if _puzzle_loader is None:
394
+ _puzzle_loader = MiniGridPuzzleLoader()
395
+ return _puzzle_loader
396
+
397
+
398
+ # Convenience functions
399
+ def get_puzzles_by_difficulty(difficulty: str) -> List[MiniGridPuzzle]:
400
+ """Convenience function to get puzzles by difficulty."""
401
+ return get_puzzle_loader().get_puzzles_by_difficulty(difficulty)
402
+
403
+
404
+ def get_puzzle_by_seed(difficulty: str, seed: int) -> Optional[MiniGridPuzzle]:
405
+ """Convenience function to get a puzzle by seed."""
406
+ return get_puzzle_loader().get_puzzle_by_seed(difficulty, seed)
407
+
408
+
409
+ def get_puzzle_by_index(difficulty: str, index: int) -> Optional[MiniGridPuzzle]:
410
+ """Convenience function to get a puzzle by index."""
411
+ return get_puzzle_loader().get_puzzle_by_index(difficulty, index)
412
+
413
+
414
+ def get_available_difficulties() -> List[str]:
415
+ """Convenience function to get available difficulties."""
416
+ return get_puzzle_loader().get_available_difficulties()