synth-ai 0.2.4.dev4__py3-none-any.whl → 0.2.4.dev5__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.
- synth_ai/environments/examples/__init__.py +1 -0
- synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
- synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
- synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
- synth_ai/environments/examples/crafter_classic/engine.py +575 -0
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +266 -0
- synth_ai/environments/examples/crafter_classic/environment.py +364 -0
- synth_ai/environments/examples/crafter_classic/taskset.py +233 -0
- synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +229 -0
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +298 -0
- synth_ai/environments/examples/crafter_custom/__init__.py +4 -0
- synth_ai/environments/examples/crafter_custom/crafter/__init__.py +7 -0
- synth_ai/environments/examples/crafter_custom/crafter/config.py +182 -0
- synth_ai/environments/examples/crafter_custom/crafter/constants.py +8 -0
- synth_ai/environments/examples/crafter_custom/crafter/engine.py +269 -0
- synth_ai/environments/examples/crafter_custom/crafter/env.py +266 -0
- synth_ai/environments/examples/crafter_custom/crafter/objects.py +418 -0
- synth_ai/environments/examples/crafter_custom/crafter/recorder.py +187 -0
- synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +119 -0
- synth_ai/environments/examples/crafter_custom/dataset_builder.py +373 -0
- synth_ai/environments/examples/crafter_custom/environment.py +312 -0
- synth_ai/environments/examples/crafter_custom/run_dataset.py +305 -0
- synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
- synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
- synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
- synth_ai/environments/examples/enron/engine.py +291 -0
- synth_ai/environments/examples/enron/environment.py +165 -0
- synth_ai/environments/examples/enron/taskset.py +112 -0
- synth_ai/environments/examples/minigrid/__init__.py +48 -0
- synth_ai/environments/examples/minigrid/engine.py +589 -0
- synth_ai/environments/examples/minigrid/environment.py +274 -0
- synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
- synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
- synth_ai/environments/examples/minigrid/taskset.py +583 -0
- synth_ai/environments/examples/nethack/__init__.py +7 -0
- synth_ai/environments/examples/nethack/achievements.py +337 -0
- synth_ai/environments/examples/nethack/engine.py +738 -0
- synth_ai/environments/examples/nethack/environment.py +255 -0
- synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
- synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
- synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
- synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
- synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
- synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
- synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
- synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
- synth_ai/environments/examples/nethack/taskset.py +323 -0
- synth_ai/environments/examples/red/__init__.py +7 -0
- synth_ai/environments/examples/red/config_logging.py +110 -0
- synth_ai/environments/examples/red/engine.py +693 -0
- synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
- synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
- synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
- synth_ai/environments/examples/red/environment.py +235 -0
- synth_ai/environments/examples/red/taskset.py +77 -0
- synth_ai/environments/examples/sokoban/__init__.py +1 -0
- synth_ai/environments/examples/sokoban/engine.py +675 -0
- synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
- synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
- synth_ai/environments/examples/sokoban/environment.py +228 -0
- synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
- synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
- synth_ai/environments/examples/sokoban/taskset.py +425 -0
- synth_ai/environments/examples/tictactoe/__init__.py +1 -0
- synth_ai/environments/examples/tictactoe/engine.py +368 -0
- synth_ai/environments/examples/tictactoe/environment.py +239 -0
- synth_ai/environments/examples/tictactoe/taskset.py +214 -0
- synth_ai/environments/examples/verilog/__init__.py +10 -0
- synth_ai/environments/examples/verilog/engine.py +328 -0
- synth_ai/environments/examples/verilog/environment.py +349 -0
- synth_ai/environments/examples/verilog/taskset.py +418 -0
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/METADATA +1 -1
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/RECORD +104 -6
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,311 @@
|
|
1
|
+
"""
|
2
|
+
Puzzle loader for pre-generated verified Sokoban puzzles.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import random
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Dict, List, Optional, Tuple, Any
|
10
|
+
from dataclasses import dataclass
|
11
|
+
import numpy as np
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class SokobanPuzzle:
|
18
|
+
"""Represents a verified solvable Sokoban puzzle."""
|
19
|
+
|
20
|
+
id: str
|
21
|
+
difficulty: str
|
22
|
+
num_boxes: int
|
23
|
+
dim_room: Tuple[int, int]
|
24
|
+
room_fixed: List[List[int]]
|
25
|
+
room_state: List[List[int]]
|
26
|
+
box_mapping: Dict[str, Any]
|
27
|
+
solution_path: List[int]
|
28
|
+
solution_length: int
|
29
|
+
generation_seed: int
|
30
|
+
max_steps: int
|
31
|
+
|
32
|
+
def to_numpy(self) -> Tuple[np.ndarray, np.ndarray]:
|
33
|
+
"""Convert room data to numpy arrays for use with the engine."""
|
34
|
+
return np.array(self.room_fixed), np.array(self.room_state)
|
35
|
+
|
36
|
+
def to_engine_snapshot(self) -> Dict[str, Any]:
|
37
|
+
"""Convert puzzle to engine snapshot format."""
|
38
|
+
return {
|
39
|
+
"dim_room": list(self.dim_room),
|
40
|
+
"room_fixed": self.room_fixed,
|
41
|
+
"room_state": self.room_state,
|
42
|
+
"box_mapping": self.box_mapping,
|
43
|
+
"boxes_on_target": self._count_boxes_on_target(),
|
44
|
+
"max_steps": self.max_steps,
|
45
|
+
"num_boxes": self.num_boxes,
|
46
|
+
}
|
47
|
+
|
48
|
+
def _count_boxes_on_target(self) -> int:
|
49
|
+
"""Count boxes currently on targets (value 3)."""
|
50
|
+
room_state = np.array(self.room_state)
|
51
|
+
return int(np.sum(room_state == 3))
|
52
|
+
|
53
|
+
|
54
|
+
class SokobanPuzzleLoader:
|
55
|
+
"""Manages loading and accessing pre-generated Sokoban puzzles."""
|
56
|
+
|
57
|
+
def __init__(self, puzzle_file_path: Optional[Path] = None):
|
58
|
+
"""
|
59
|
+
Initialize the puzzle loader.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
puzzle_file_path: Path to the JSON file containing puzzles.
|
63
|
+
If None, uses default path.
|
64
|
+
"""
|
65
|
+
if puzzle_file_path is None:
|
66
|
+
puzzle_file_path = Path(__file__).parent / "verified_puzzles.json"
|
67
|
+
|
68
|
+
self.puzzle_file_path = puzzle_file_path
|
69
|
+
self.puzzles: Dict[str, List[SokobanPuzzle]] = {}
|
70
|
+
self.metadata: Dict[str, Any] = {}
|
71
|
+
self._loaded = False
|
72
|
+
|
73
|
+
def load_puzzles(self) -> None:
|
74
|
+
"""Load puzzles from the JSON file."""
|
75
|
+
if self._loaded:
|
76
|
+
return
|
77
|
+
|
78
|
+
if not self.puzzle_file_path.exists():
|
79
|
+
raise FileNotFoundError(f"Puzzle file not found: {self.puzzle_file_path}")
|
80
|
+
|
81
|
+
try:
|
82
|
+
with open(self.puzzle_file_path, "r") as f:
|
83
|
+
data = json.load(f)
|
84
|
+
|
85
|
+
self.metadata = data.get("metadata", {})
|
86
|
+
puzzle_data = data.get("puzzles", {})
|
87
|
+
|
88
|
+
# Convert to SokobanPuzzle objects
|
89
|
+
for difficulty, puzzle_list in puzzle_data.items():
|
90
|
+
self.puzzles[difficulty] = []
|
91
|
+
for puzzle_dict in puzzle_list:
|
92
|
+
puzzle = SokobanPuzzle(
|
93
|
+
id=puzzle_dict["id"],
|
94
|
+
difficulty=puzzle_dict["difficulty"],
|
95
|
+
num_boxes=puzzle_dict["num_boxes"],
|
96
|
+
dim_room=tuple(puzzle_dict["dim_room"]),
|
97
|
+
room_fixed=puzzle_dict["room_fixed"],
|
98
|
+
room_state=puzzle_dict["room_state"],
|
99
|
+
box_mapping=puzzle_dict["box_mapping"],
|
100
|
+
solution_path=puzzle_dict["solution_path"],
|
101
|
+
solution_length=puzzle_dict["solution_length"],
|
102
|
+
generation_seed=puzzle_dict["generation_seed"],
|
103
|
+
max_steps=puzzle_dict["max_steps"],
|
104
|
+
)
|
105
|
+
self.puzzles[difficulty].append(puzzle)
|
106
|
+
|
107
|
+
self._loaded = True
|
108
|
+
logger.info(
|
109
|
+
f"Loaded {self.get_total_puzzle_count()} puzzles from {self.puzzle_file_path}"
|
110
|
+
)
|
111
|
+
|
112
|
+
except Exception as e:
|
113
|
+
logger.error(f"Error loading puzzles: {e}")
|
114
|
+
raise
|
115
|
+
|
116
|
+
def get_puzzle_by_id(self, puzzle_id: str) -> Optional[SokobanPuzzle]:
|
117
|
+
"""Get a specific puzzle by its ID."""
|
118
|
+
self.load_puzzles()
|
119
|
+
|
120
|
+
for difficulty_puzzles in self.puzzles.values():
|
121
|
+
for puzzle in difficulty_puzzles:
|
122
|
+
if puzzle.id == puzzle_id:
|
123
|
+
return puzzle
|
124
|
+
return None
|
125
|
+
|
126
|
+
def get_puzzles_by_difficulty(self, difficulty: str) -> List[SokobanPuzzle]:
|
127
|
+
"""Get all puzzles for a specific difficulty level."""
|
128
|
+
self.load_puzzles()
|
129
|
+
return self.puzzles.get(difficulty, [])
|
130
|
+
|
131
|
+
def get_random_puzzle(self, difficulty: str) -> Optional[SokobanPuzzle]:
|
132
|
+
"""Get a random puzzle from the specified difficulty level."""
|
133
|
+
puzzles = self.get_puzzles_by_difficulty(difficulty)
|
134
|
+
if not puzzles:
|
135
|
+
return None
|
136
|
+
return random.choice(puzzles)
|
137
|
+
|
138
|
+
def get_puzzle_by_index(self, difficulty: str, index: int) -> Optional[SokobanPuzzle]:
|
139
|
+
"""Get a puzzle by its index within a difficulty level."""
|
140
|
+
puzzles = self.get_puzzles_by_difficulty(difficulty)
|
141
|
+
if 0 <= index < len(puzzles):
|
142
|
+
return puzzles[index]
|
143
|
+
return None
|
144
|
+
|
145
|
+
def get_puzzle_by_seed(self, difficulty: str, seed: int) -> Optional[SokobanPuzzle]:
|
146
|
+
"""
|
147
|
+
Get a puzzle deterministically using a seed via modular arithmetic.
|
148
|
+
Same seed will always return the same puzzle for a given difficulty.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
difficulty: The difficulty level
|
152
|
+
seed: Integer seed for deterministic selection
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
SokobanPuzzle or None if no puzzles available
|
156
|
+
"""
|
157
|
+
puzzles = self.get_puzzles_by_difficulty(difficulty)
|
158
|
+
if not puzzles:
|
159
|
+
return None
|
160
|
+
|
161
|
+
# Use modular arithmetic to map seed to puzzle index
|
162
|
+
index = seed % len(puzzles)
|
163
|
+
return puzzles[index]
|
164
|
+
|
165
|
+
def get_available_difficulties(self) -> List[str]:
|
166
|
+
"""Get list of available difficulty levels."""
|
167
|
+
self.load_puzzles()
|
168
|
+
return list(self.puzzles.keys())
|
169
|
+
|
170
|
+
def get_puzzle_count(self, difficulty: str) -> int:
|
171
|
+
"""Get the number of puzzles for a specific difficulty."""
|
172
|
+
return len(self.get_puzzles_by_difficulty(difficulty))
|
173
|
+
|
174
|
+
def get_total_puzzle_count(self) -> int:
|
175
|
+
"""Get the total number of puzzles across all difficulties."""
|
176
|
+
self.load_puzzles()
|
177
|
+
return sum(len(puzzles) for puzzles in self.puzzles.values())
|
178
|
+
|
179
|
+
def get_puzzles_by_criteria(
|
180
|
+
self,
|
181
|
+
difficulty: Optional[str] = None,
|
182
|
+
num_boxes: Optional[int] = None,
|
183
|
+
min_solution_length: Optional[int] = None,
|
184
|
+
max_solution_length: Optional[int] = None,
|
185
|
+
max_results: Optional[int] = None,
|
186
|
+
) -> List[SokobanPuzzle]:
|
187
|
+
"""
|
188
|
+
Get puzzles matching specific criteria.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
difficulty: Filter by difficulty level
|
192
|
+
num_boxes: Filter by number of boxes
|
193
|
+
min_solution_length: Minimum solution length
|
194
|
+
max_solution_length: Maximum solution length
|
195
|
+
max_results: Maximum number of results to return
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
List of matching puzzles
|
199
|
+
"""
|
200
|
+
self.load_puzzles()
|
201
|
+
|
202
|
+
all_puzzles = []
|
203
|
+
if difficulty:
|
204
|
+
all_puzzles = self.get_puzzles_by_difficulty(difficulty)
|
205
|
+
else:
|
206
|
+
for difficulty_puzzles in self.puzzles.values():
|
207
|
+
all_puzzles.extend(difficulty_puzzles)
|
208
|
+
|
209
|
+
# Apply filters
|
210
|
+
filtered_puzzles = []
|
211
|
+
for puzzle in all_puzzles:
|
212
|
+
if num_boxes is not None and puzzle.num_boxes != num_boxes:
|
213
|
+
continue
|
214
|
+
if min_solution_length is not None and puzzle.solution_length < min_solution_length:
|
215
|
+
continue
|
216
|
+
if max_solution_length is not None and puzzle.solution_length > max_solution_length:
|
217
|
+
continue
|
218
|
+
filtered_puzzles.append(puzzle)
|
219
|
+
|
220
|
+
# Limit results
|
221
|
+
if max_results is not None:
|
222
|
+
filtered_puzzles = filtered_puzzles[:max_results]
|
223
|
+
|
224
|
+
return filtered_puzzles
|
225
|
+
|
226
|
+
def get_metadata(self) -> Dict[str, Any]:
|
227
|
+
"""Get metadata about the puzzle set."""
|
228
|
+
self.load_puzzles()
|
229
|
+
return self.metadata
|
230
|
+
|
231
|
+
def get_metadata_for_filtering(self) -> Dict[str, Any]:
|
232
|
+
"""Get metadata to help with filtering across environments."""
|
233
|
+
self.load_puzzles()
|
234
|
+
|
235
|
+
return {
|
236
|
+
"environment_type": "sokoban",
|
237
|
+
"total_puzzles": self.get_total_puzzle_count(),
|
238
|
+
"difficulties": self.get_available_difficulties(),
|
239
|
+
"difficulty_counts": {
|
240
|
+
difficulty: len(puzzles) for difficulty, puzzles in self.puzzles.items()
|
241
|
+
},
|
242
|
+
"features": {
|
243
|
+
"has_boxes": True,
|
244
|
+
"has_targets": True,
|
245
|
+
"has_player": True,
|
246
|
+
"grid_based": True,
|
247
|
+
"puzzle_type": "box_pushing",
|
248
|
+
},
|
249
|
+
"difficulty_ranges": {
|
250
|
+
"ultra_easy": {"boxes": 1, "grid_size": (5, 5), "solution_length": (3, 8)},
|
251
|
+
"easy": {"boxes": 1, "grid_size": (6, 6), "solution_length": (8, 15)},
|
252
|
+
"medium": {"boxes": 2, "grid_size": (7, 7), "solution_length": (15, 30)},
|
253
|
+
"hard": {"boxes": 3, "grid_size": (8, 8), "solution_length": (30, 60)},
|
254
|
+
},
|
255
|
+
}
|
256
|
+
|
257
|
+
def print_summary(self) -> None:
|
258
|
+
"""Print a summary of loaded puzzles."""
|
259
|
+
self.load_puzzles()
|
260
|
+
|
261
|
+
print(f"Sokoban Puzzle Summary:")
|
262
|
+
print(f"Total puzzles: {self.get_total_puzzle_count()}")
|
263
|
+
print(f"Difficulties: {', '.join(self.get_available_difficulties())}")
|
264
|
+
print()
|
265
|
+
|
266
|
+
for difficulty in self.get_available_difficulties():
|
267
|
+
puzzles = self.get_puzzles_by_difficulty(difficulty)
|
268
|
+
if puzzles:
|
269
|
+
avg_solution_length = sum(p.solution_length for p in puzzles) / len(puzzles)
|
270
|
+
min_solution_length = min(p.solution_length for p in puzzles)
|
271
|
+
max_solution_length = max(p.solution_length for p in puzzles)
|
272
|
+
|
273
|
+
print(f"{difficulty}:")
|
274
|
+
print(f" Count: {len(puzzles)}")
|
275
|
+
print(f" Avg solution length: {avg_solution_length:.1f}")
|
276
|
+
print(f" Solution length range: {min_solution_length}-{max_solution_length}")
|
277
|
+
print(f" Boxes: {puzzles[0].num_boxes}")
|
278
|
+
print(f" Room size: {puzzles[0].dim_room}")
|
279
|
+
print()
|
280
|
+
|
281
|
+
|
282
|
+
# Global instance for easy access
|
283
|
+
_global_loader = None
|
284
|
+
|
285
|
+
|
286
|
+
def get_puzzle_loader() -> SokobanPuzzleLoader:
|
287
|
+
"""Get the global puzzle loader instance."""
|
288
|
+
global _global_loader
|
289
|
+
if _global_loader is None:
|
290
|
+
_global_loader = SokobanPuzzleLoader()
|
291
|
+
return _global_loader
|
292
|
+
|
293
|
+
|
294
|
+
def get_puzzle_by_id(puzzle_id: str) -> Optional[SokobanPuzzle]:
|
295
|
+
"""Convenience function to get a puzzle by ID."""
|
296
|
+
return get_puzzle_loader().get_puzzle_by_id(puzzle_id)
|
297
|
+
|
298
|
+
|
299
|
+
def get_random_puzzle(difficulty: str) -> Optional[SokobanPuzzle]:
|
300
|
+
"""Convenience function to get a random puzzle."""
|
301
|
+
return get_puzzle_loader().get_random_puzzle(difficulty)
|
302
|
+
|
303
|
+
|
304
|
+
def get_puzzle_by_index(difficulty: str, index: int) -> Optional[SokobanPuzzle]:
|
305
|
+
"""Convenience function to get a puzzle by index."""
|
306
|
+
return get_puzzle_loader().get_puzzle_by_index(difficulty, index)
|
307
|
+
|
308
|
+
|
309
|
+
def get_puzzle_by_seed(difficulty: str, seed: int) -> Optional[SokobanPuzzle]:
|
310
|
+
"""Convenience function to get a puzzle by seed."""
|
311
|
+
return get_puzzle_loader().get_puzzle_by_seed(difficulty, seed)
|