chuk-puzzles-gym 0.9__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 (112) hide show
  1. chuk_puzzles_gym/__init__.py +19 -0
  2. chuk_puzzles_gym/constants.py +9 -0
  3. chuk_puzzles_gym/eval.py +763 -0
  4. chuk_puzzles_gym/export/__init__.py +20 -0
  5. chuk_puzzles_gym/export/dataset.py +376 -0
  6. chuk_puzzles_gym/games/__init__.py +94 -0
  7. chuk_puzzles_gym/games/_base/__init__.py +6 -0
  8. chuk_puzzles_gym/games/_base/commands.py +91 -0
  9. chuk_puzzles_gym/games/_base/game.py +337 -0
  10. chuk_puzzles_gym/games/binary/__init__.py +6 -0
  11. chuk_puzzles_gym/games/binary/config.py +23 -0
  12. chuk_puzzles_gym/games/binary/game.py +434 -0
  13. chuk_puzzles_gym/games/bridges/__init__.py +6 -0
  14. chuk_puzzles_gym/games/bridges/config.py +24 -0
  15. chuk_puzzles_gym/games/bridges/game.py +489 -0
  16. chuk_puzzles_gym/games/einstein/__init__.py +6 -0
  17. chuk_puzzles_gym/games/einstein/config.py +23 -0
  18. chuk_puzzles_gym/games/einstein/constants.py +13 -0
  19. chuk_puzzles_gym/games/einstein/game.py +366 -0
  20. chuk_puzzles_gym/games/einstein/models.py +35 -0
  21. chuk_puzzles_gym/games/fillomino/__init__.py +6 -0
  22. chuk_puzzles_gym/games/fillomino/config.py +24 -0
  23. chuk_puzzles_gym/games/fillomino/game.py +516 -0
  24. chuk_puzzles_gym/games/futoshiki/__init__.py +6 -0
  25. chuk_puzzles_gym/games/futoshiki/config.py +23 -0
  26. chuk_puzzles_gym/games/futoshiki/game.py +391 -0
  27. chuk_puzzles_gym/games/hidato/__init__.py +6 -0
  28. chuk_puzzles_gym/games/hidato/config.py +24 -0
  29. chuk_puzzles_gym/games/hidato/game.py +403 -0
  30. chuk_puzzles_gym/games/hitori/__init__.py +6 -0
  31. chuk_puzzles_gym/games/hitori/config.py +23 -0
  32. chuk_puzzles_gym/games/hitori/game.py +451 -0
  33. chuk_puzzles_gym/games/kakuro/__init__.py +6 -0
  34. chuk_puzzles_gym/games/kakuro/config.py +24 -0
  35. chuk_puzzles_gym/games/kakuro/game.py +399 -0
  36. chuk_puzzles_gym/games/kenken/__init__.py +6 -0
  37. chuk_puzzles_gym/games/kenken/config.py +24 -0
  38. chuk_puzzles_gym/games/kenken/enums.py +13 -0
  39. chuk_puzzles_gym/games/kenken/game.py +486 -0
  40. chuk_puzzles_gym/games/kenken/models.py +15 -0
  41. chuk_puzzles_gym/games/killer_sudoku/__init__.py +6 -0
  42. chuk_puzzles_gym/games/killer_sudoku/config.py +23 -0
  43. chuk_puzzles_gym/games/killer_sudoku/game.py +502 -0
  44. chuk_puzzles_gym/games/killer_sudoku/models.py +15 -0
  45. chuk_puzzles_gym/games/knapsack/__init__.py +6 -0
  46. chuk_puzzles_gym/games/knapsack/config.py +24 -0
  47. chuk_puzzles_gym/games/knapsack/enums.py +10 -0
  48. chuk_puzzles_gym/games/knapsack/game.py +340 -0
  49. chuk_puzzles_gym/games/knapsack/models.py +13 -0
  50. chuk_puzzles_gym/games/lights_out/__init__.py +6 -0
  51. chuk_puzzles_gym/games/lights_out/config.py +24 -0
  52. chuk_puzzles_gym/games/lights_out/game.py +249 -0
  53. chuk_puzzles_gym/games/logic_grid/__init__.py +6 -0
  54. chuk_puzzles_gym/games/logic_grid/config.py +24 -0
  55. chuk_puzzles_gym/games/logic_grid/constants.py +12 -0
  56. chuk_puzzles_gym/games/logic_grid/game.py +333 -0
  57. chuk_puzzles_gym/games/logic_grid/models.py +24 -0
  58. chuk_puzzles_gym/games/mastermind/__init__.py +6 -0
  59. chuk_puzzles_gym/games/mastermind/config.py +25 -0
  60. chuk_puzzles_gym/games/mastermind/game.py +297 -0
  61. chuk_puzzles_gym/games/minesweeper/__init__.py +6 -0
  62. chuk_puzzles_gym/games/minesweeper/config.py +24 -0
  63. chuk_puzzles_gym/games/minesweeper/enums.py +12 -0
  64. chuk_puzzles_gym/games/minesweeper/game.py +432 -0
  65. chuk_puzzles_gym/games/nonogram/__init__.py +6 -0
  66. chuk_puzzles_gym/games/nonogram/config.py +23 -0
  67. chuk_puzzles_gym/games/nonogram/game.py +296 -0
  68. chuk_puzzles_gym/games/nurikabe/__init__.py +6 -0
  69. chuk_puzzles_gym/games/nurikabe/config.py +24 -0
  70. chuk_puzzles_gym/games/nurikabe/enums.py +14 -0
  71. chuk_puzzles_gym/games/nurikabe/game.py +586 -0
  72. chuk_puzzles_gym/games/scheduler/__init__.py +6 -0
  73. chuk_puzzles_gym/games/scheduler/config.py +25 -0
  74. chuk_puzzles_gym/games/scheduler/constants.py +15 -0
  75. chuk_puzzles_gym/games/scheduler/enums.py +10 -0
  76. chuk_puzzles_gym/games/scheduler/game.py +431 -0
  77. chuk_puzzles_gym/games/scheduler/models.py +14 -0
  78. chuk_puzzles_gym/games/shikaku/__init__.py +6 -0
  79. chuk_puzzles_gym/games/shikaku/config.py +24 -0
  80. chuk_puzzles_gym/games/shikaku/game.py +419 -0
  81. chuk_puzzles_gym/games/slitherlink/__init__.py +6 -0
  82. chuk_puzzles_gym/games/slitherlink/config.py +23 -0
  83. chuk_puzzles_gym/games/slitherlink/game.py +386 -0
  84. chuk_puzzles_gym/games/sokoban/__init__.py +6 -0
  85. chuk_puzzles_gym/games/sokoban/config.py +24 -0
  86. chuk_puzzles_gym/games/sokoban/game.py +671 -0
  87. chuk_puzzles_gym/games/star_battle/__init__.py +6 -0
  88. chuk_puzzles_gym/games/star_battle/config.py +24 -0
  89. chuk_puzzles_gym/games/star_battle/game.py +390 -0
  90. chuk_puzzles_gym/games/sudoku/__init__.py +7 -0
  91. chuk_puzzles_gym/games/sudoku/commands.py +96 -0
  92. chuk_puzzles_gym/games/sudoku/config.py +22 -0
  93. chuk_puzzles_gym/games/sudoku/game.py +328 -0
  94. chuk_puzzles_gym/games/tents/__init__.py +6 -0
  95. chuk_puzzles_gym/games/tents/config.py +24 -0
  96. chuk_puzzles_gym/games/tents/game.py +416 -0
  97. chuk_puzzles_gym/gym_env.py +465 -0
  98. chuk_puzzles_gym/models/__init__.py +47 -0
  99. chuk_puzzles_gym/models/base.py +30 -0
  100. chuk_puzzles_gym/models/config.py +11 -0
  101. chuk_puzzles_gym/models/enums.py +104 -0
  102. chuk_puzzles_gym/models/evaluation.py +487 -0
  103. chuk_puzzles_gym/models/games.py +12 -0
  104. chuk_puzzles_gym/server.py +1171 -0
  105. chuk_puzzles_gym/trace/__init__.py +10 -0
  106. chuk_puzzles_gym/trace/generator.py +726 -0
  107. chuk_puzzles_gym/utils/__init__.py +4 -0
  108. chuk_puzzles_gym-0.9.dist-info/METADATA +1471 -0
  109. chuk_puzzles_gym-0.9.dist-info/RECORD +112 -0
  110. chuk_puzzles_gym-0.9.dist-info/WHEEL +5 -0
  111. chuk_puzzles_gym-0.9.dist-info/entry_points.txt +4 -0
  112. chuk_puzzles_gym-0.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,419 @@
1
+ """Shikaku (Rectangles) puzzle game implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ...models import DifficultyProfile, MoveResult
6
+ from .._base import PuzzleGame
7
+
8
+
9
+ class ShikakuGame(PuzzleGame):
10
+ """Shikaku (Rectangles) puzzle game.
11
+
12
+ Divide the grid into rectangles such that:
13
+ - Each rectangle contains exactly one number
14
+ - The number indicates the area of that rectangle
15
+ - All cells must be covered by rectangles
16
+ - Rectangles cannot overlap
17
+ """
18
+
19
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
20
+ """Initialize a new Shikaku game.
21
+
22
+ Args:
23
+ difficulty: Game difficulty level (easy, medium, hard)
24
+ """
25
+ super().__init__(difficulty, seed, **kwargs)
26
+
27
+ from ...models import DifficultyLevel
28
+
29
+ # Set grid size based on difficulty
30
+ self.size = {
31
+ DifficultyLevel.EASY.value: 6,
32
+ DifficultyLevel.MEDIUM.value: 8,
33
+ DifficultyLevel.HARD.value: 10,
34
+ }.get(self.difficulty.value, 6)
35
+
36
+ # Grid stores the clue numbers (0 = no clue)
37
+ self.grid: list[list[int]] = [[0 for _ in range(self.size)] for _ in range(self.size)]
38
+
39
+ # Solution stores rectangle IDs (each rectangle has a unique ID)
40
+ self.solution: list[list[int]] = [[0 for _ in range(self.size)] for _ in range(self.size)]
41
+
42
+ # Player's rectangles (rectangle_id -> list of (row, col) cells)
43
+ self.rectangles: dict[int, list[tuple[int, int]]] = {}
44
+ self.next_rect_id = 1
45
+
46
+ # Store clue positions for validation
47
+ self.clues: dict[tuple[int, int], int] = {}
48
+
49
+ @property
50
+ def name(self) -> str:
51
+ """The display name of this puzzle type."""
52
+ return "Shikaku"
53
+
54
+ @property
55
+ def description(self) -> str:
56
+ """A one-line description of this puzzle type."""
57
+ return "Divide grid into rectangles matching the given areas"
58
+
59
+ @property
60
+ def constraint_types(self) -> list[str]:
61
+ """Constraint types demonstrated by this puzzle."""
62
+ return ["partition", "area_constraints", "rectangle_tiling", "non_overlapping", "coverage"]
63
+
64
+ @property
65
+ def business_analogies(self) -> list[str]:
66
+ """Business problems this puzzle models."""
67
+ return ["space_allocation", "territory_division", "resource_partitioning", "layout_optimization"]
68
+
69
+ @property
70
+ def complexity_profile(self) -> dict[str, str]:
71
+ """Complexity profile of this puzzle."""
72
+ return {"reasoning_type": "deductive", "search_space": "large", "constraint_density": "moderate"}
73
+
74
+ @property
75
+ def optimal_steps(self) -> int | None:
76
+ """Minimum steps = rectangles to create."""
77
+ if not hasattr(self, "solution") or not self.solution:
78
+ return None
79
+ return max(max(row) for row in self.solution) if self.solution else None
80
+
81
+ @property
82
+ def difficulty_profile(self) -> "DifficultyProfile":
83
+ """Difficulty characteristics for Shikaku."""
84
+ from ...models import DifficultyLevel
85
+
86
+ logic_depth = {
87
+ DifficultyLevel.EASY.value: 2,
88
+ DifficultyLevel.MEDIUM.value: 4,
89
+ DifficultyLevel.HARD.value: 5,
90
+ }.get(self.difficulty.value, 3)
91
+ return DifficultyProfile(
92
+ logic_depth=logic_depth,
93
+ branching_factor=5.0, # Many rectangle possibilities
94
+ state_observability=1.0,
95
+ constraint_density=0.5,
96
+ )
97
+
98
+ async def generate_puzzle(self) -> None:
99
+ """Generate a new Shikaku puzzle with retry logic."""
100
+ max_attempts = 50
101
+
102
+ for _attempt in range(max_attempts):
103
+ # Reset state
104
+ self.grid = [[0 for _ in range(self.size)] for _ in range(self.size)]
105
+ self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
106
+ self.clues = {}
107
+
108
+ # Step 1: Generate solution by creating rectangles
109
+ if self._generate_rectangles():
110
+ # Step 2: Validate puzzle
111
+ if self._validate_puzzle():
112
+ self.game_started = True
113
+ return
114
+
115
+ # Fallback: use last attempt
116
+ self.game_started = True
117
+
118
+ def _generate_rectangles(self) -> bool:
119
+ """Generate rectangles to fill the grid."""
120
+ rect_id = 1
121
+ remaining_cells = {(r, c) for r in range(self.size) for c in range(self.size)}
122
+
123
+ # Use strategic starting points (grid positions) for better coverage
124
+ start_positions = []
125
+ step = max(2, self.size // 3)
126
+ for r in range(0, self.size, step):
127
+ for c in range(0, self.size, step):
128
+ start_positions.append((r, c))
129
+ self._rng.shuffle(start_positions)
130
+
131
+ attempts = 0
132
+ max_rect_attempts = 100
133
+
134
+ while remaining_cells and attempts < max_rect_attempts:
135
+ attempts += 1
136
+
137
+ # Try strategic positions first, then random
138
+ if start_positions:
139
+ # Filter start positions to only uncovered cells
140
+ valid_starts = [pos for pos in start_positions if pos in remaining_cells]
141
+ if valid_starts:
142
+ r, c = self._rng.choice(valid_starts)
143
+ else:
144
+ r, c = self._rng.choice(list(remaining_cells))
145
+ else:
146
+ r, c = self._rng.choice(list(remaining_cells))
147
+
148
+ # Determine maximum rectangle size
149
+ max_width = 1
150
+ while c + max_width < self.size and (r, c + max_width) in remaining_cells:
151
+ max_width += 1
152
+
153
+ max_height = 1
154
+ while r + max_height < self.size and (r + max_height, c) in remaining_cells:
155
+ max_height += 1
156
+
157
+ # Choose dimensions (prefer reasonable sizes)
158
+ max_dim = 4
159
+ width = self._rng.randint(1, min(max_width, max_dim))
160
+ height = self._rng.randint(1, min(max_height, max_dim))
161
+
162
+ # Ensure the rectangle fits
163
+ valid = True
164
+ rect_cells = []
165
+ for dr in range(height):
166
+ for dc in range(width):
167
+ nr, nc = r + dr, c + dc
168
+ if nr >= self.size or nc >= self.size or (nr, nc) not in remaining_cells:
169
+ valid = False
170
+ break
171
+ rect_cells.append((nr, nc))
172
+ if not valid:
173
+ break
174
+
175
+ if valid and rect_cells:
176
+ # Mark these cells with the rectangle ID
177
+ for nr, nc in rect_cells:
178
+ self.solution[nr][nc] = rect_id
179
+ remaining_cells.remove((nr, nc))
180
+
181
+ # Place the clue number in a random cell within the rectangle
182
+ clue_r, clue_c = self._rng.choice(rect_cells)
183
+ area = width * height
184
+ self.grid[clue_r][clue_c] = area
185
+ self.clues[(clue_r, clue_c)] = area
186
+
187
+ rect_id += 1
188
+ else:
189
+ # If we can't create a valid rectangle, just use a single cell
190
+ self.solution[r][c] = rect_id
191
+ self.grid[r][c] = 1
192
+ self.clues[(r, c)] = 1
193
+ remaining_cells.remove((r, c))
194
+ rect_id += 1
195
+
196
+ return len(remaining_cells) == 0
197
+
198
+ def _validate_puzzle(self) -> bool:
199
+ """Validate that the generated puzzle is valid."""
200
+ # Check all cells are covered
201
+ for r in range(self.size):
202
+ for c in range(self.size):
203
+ if self.solution[r][c] == 0:
204
+ return False
205
+
206
+ # Check each clue matches its rectangle area
207
+ for (clue_r, clue_c), area in self.clues.items():
208
+ rect_id = self.solution[clue_r][clue_c]
209
+ rect_cells = sum(1 for r in range(self.size) for c in range(self.size) if self.solution[r][c] == rect_id)
210
+
211
+ if rect_cells != area:
212
+ return False
213
+
214
+ # Check no rectangle has multiple clues
215
+ rect_clue_count: dict[int, int] = {}
216
+ for (r, c), _area in self.clues.items():
217
+ rect_id = self.solution[r][c]
218
+ rect_clue_count[rect_id] = rect_clue_count.get(rect_id, 0) + 1
219
+
220
+ for count in rect_clue_count.values():
221
+ if count != 1:
222
+ return False
223
+
224
+ return True
225
+
226
+ async def validate_move(self, *args: Any, **kwargs: Any) -> MoveResult:
227
+ """Validate a rectangle placement move.
228
+
229
+ Args:
230
+ args[0]: Top-left row (1-indexed)
231
+ args[1]: Top-left column (1-indexed)
232
+ args[2]: Bottom-right row (1-indexed)
233
+ args[3]: Bottom-right column (1-indexed)
234
+
235
+ Returns:
236
+ MoveResult containing success status and message
237
+ """
238
+ if len(args) < 4:
239
+ return MoveResult(success=False, message="Usage: place <top_row> <top_col> <bottom_row> <bottom_col>")
240
+
241
+ try:
242
+ r1, c1, r2, c2 = int(args[0]) - 1, int(args[1]) - 1, int(args[2]) - 1, int(args[3]) - 1
243
+ except (ValueError, IndexError):
244
+ return MoveResult(success=False, message="Invalid coordinates")
245
+
246
+ # Normalize coordinates
247
+ if r1 > r2:
248
+ r1, r2 = r2, r1
249
+ if c1 > c2:
250
+ c1, c2 = c2, c1
251
+
252
+ # Validate coordinates
253
+ if not (0 <= r1 <= r2 < self.size and 0 <= c1 <= c2 < self.size):
254
+ return MoveResult(success=False, message="Coordinates out of range")
255
+
256
+ # Check if cells are already covered
257
+ for r in range(r1, r2 + 1):
258
+ for c in range(c1, c2 + 1):
259
+ for rect_id, cells in self.rectangles.items():
260
+ if (r, c) in cells:
261
+ return MoveResult(
262
+ success=False,
263
+ message=f"Cell ({r + 1},{c + 1}) is already covered by rectangle {rect_id}",
264
+ )
265
+
266
+ # Find if rectangle contains exactly one clue
267
+ clue_count = 0
268
+ clue_value = 0
269
+ for r in range(r1, r2 + 1):
270
+ for c in range(c1, c2 + 1):
271
+ if (r, c) in self.clues:
272
+ clue_count += 1
273
+ clue_value = self.clues[(r, c)]
274
+
275
+ if clue_count == 0:
276
+ return MoveResult(success=False, message="Rectangle must contain exactly one clue number")
277
+ if clue_count > 1:
278
+ return MoveResult(success=False, message="Rectangle contains multiple clue numbers")
279
+
280
+ # Check if area matches the clue
281
+ width = c2 - c1 + 1
282
+ height = r2 - r1 + 1
283
+ area = width * height
284
+
285
+ if area != clue_value:
286
+ return MoveResult(success=False, message=f"Rectangle area ({area}) doesn't match clue ({clue_value})")
287
+
288
+ # Place the rectangle
289
+ cells = [(r, c) for r in range(r1, r2 + 1) for c in range(c1, c2 + 1)]
290
+ self.rectangles[self.next_rect_id] = cells
291
+ self.next_rect_id += 1
292
+ self.moves_made += 1
293
+
294
+ return MoveResult(success=True, message=f"Rectangle placed (area {area})")
295
+
296
+ def is_complete(self) -> bool:
297
+ """Check if the puzzle is completely and correctly solved."""
298
+ # Check that all cells are covered
299
+ covered_cells = set()
300
+ for cells in self.rectangles.values():
301
+ for cell in cells:
302
+ if cell in covered_cells:
303
+ return False # Overlapping rectangles
304
+ covered_cells.add(cell)
305
+
306
+ # Check if all cells are covered
307
+ total_cells = self.size * self.size
308
+ if len(covered_cells) != total_cells:
309
+ return False
310
+
311
+ # Check that each rectangle contains exactly one clue with matching area
312
+ for cells in self.rectangles.values():
313
+ clue_count = 0
314
+ clue_value = 0
315
+ for r, c in cells:
316
+ if (r, c) in self.clues:
317
+ clue_count += 1
318
+ clue_value = self.clues[(r, c)]
319
+
320
+ if clue_count != 1:
321
+ return False
322
+
323
+ if len(cells) != clue_value:
324
+ return False
325
+
326
+ return True
327
+
328
+ async def get_hint(self) -> tuple[Any, str] | None:
329
+ """Get a hint for the next move."""
330
+ # Find a rectangle from the solution that hasn't been placed yet
331
+ solution_rects: dict[int, list[tuple[int, int]]] = {}
332
+ for r in range(self.size):
333
+ for c in range(self.size):
334
+ rect_id = self.solution[r][c]
335
+ if rect_id not in solution_rects:
336
+ solution_rects[rect_id] = []
337
+ solution_rects[rect_id].append((r, c))
338
+
339
+ # Check which solution rectangles haven't been placed
340
+ for cells in solution_rects.values():
341
+ # Check if any cell in this rectangle is not yet covered
342
+ is_placed = False
343
+ for r, c in cells:
344
+ for placed_cells in self.rectangles.values():
345
+ if (r, c) in placed_cells:
346
+ is_placed = True
347
+ break
348
+ if is_placed:
349
+ break
350
+
351
+ if not is_placed:
352
+ # Found an unplaced rectangle
353
+ min_r = min(r for r, c in cells)
354
+ max_r = max(r for r, c in cells)
355
+ min_c = min(c for r, c in cells)
356
+ max_c = max(c for r, c in cells)
357
+
358
+ return (
359
+ (min_r + 1, min_c + 1, max_r + 1, max_c + 1),
360
+ f"Try rectangle from ({min_r + 1},{min_c + 1}) to ({max_r + 1},{max_c + 1})",
361
+ )
362
+
363
+ return None
364
+
365
+ def render_grid(self) -> str:
366
+ """Render the current puzzle state as ASCII art."""
367
+ lines = []
368
+
369
+ # Create a display grid showing rectangles
370
+ display = [[" . " for _ in range(self.size)] for _ in range(self.size)]
371
+
372
+ # Place clue numbers
373
+ for (r, c), value in self.clues.items():
374
+ display[r][c] = f" {value:2d}"
375
+
376
+ # Mark placed rectangles with letters
377
+ rect_ids = sorted(self.rectangles.keys())
378
+ for idx, rect_id in enumerate(rect_ids):
379
+ letter = chr(65 + (idx % 26)) # A, B, C, ...
380
+ for r, c in self.rectangles[rect_id]:
381
+ if (r, c) not in self.clues:
382
+ display[r][c] = f" {letter} "
383
+
384
+ # Header
385
+ header = " |"
386
+ for c in range(self.size):
387
+ header += f" {c + 1:2d}"
388
+ lines.append(header)
389
+ lines.append(" +" + "---" * self.size)
390
+
391
+ # Grid rows
392
+ for r in range(self.size):
393
+ row = f"{r + 1:2d}|"
394
+ for c in range(self.size):
395
+ row += display[r][c]
396
+ lines.append(row)
397
+
398
+ return "\n".join(lines)
399
+
400
+ def get_rules(self) -> str:
401
+ """Get the rules description for this puzzle type."""
402
+ return """SHIKAKU (RECTANGLES) RULES:
403
+ - Divide the grid into rectangles
404
+ - Each rectangle must contain exactly one number
405
+ - The number shows the area (width × height) of that rectangle
406
+ - All cells must be covered by rectangles
407
+ - Rectangles cannot overlap
408
+ - Placed rectangles are marked with letters (A, B, C, ...)"""
409
+
410
+ def get_commands(self) -> str:
411
+ """Get the available commands for this puzzle type."""
412
+ return """SHIKAKU COMMANDS:
413
+ place <r1> <c1> <r2> <c2> - Draw rectangle from top-left to bottom-right
414
+ Example: place 1 1 2 3 (creates 2×3 rectangle)
415
+ hint - Get a hint for the next move
416
+ check - Check if puzzle is complete
417
+ solve - Show the solution
418
+ menu - Return to main menu
419
+ quit - Exit the game"""
@@ -0,0 +1,6 @@
1
+ """Slitherlink puzzle game module."""
2
+
3
+ from .config import SlitherlinkConfig
4
+ from .game import SlitherlinkGame
5
+
6
+ __all__ = ["SlitherlinkGame", "SlitherlinkConfig"]
@@ -0,0 +1,23 @@
1
+ """Configuration for Slitherlink game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class SlitherlinkConfig(BaseModel):
9
+ """Configuration for Slitherlink game."""
10
+
11
+ difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
12
+ size: int = Field(ge=5, le=10, description="Grid size (NxN)")
13
+
14
+ @classmethod
15
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "SlitherlinkConfig":
16
+ """Create config from difficulty level."""
17
+ config_map = {
18
+ DifficultyLevel.EASY: {"size": 5},
19
+ DifficultyLevel.MEDIUM: {"size": 7},
20
+ DifficultyLevel.HARD: {"size": 10},
21
+ }
22
+ params = config_map[difficulty]
23
+ return cls(difficulty=difficulty, **params)