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,489 @@
1
+ """Bridges (Hashiwokakero) 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 BridgesGame(PuzzleGame):
10
+ """Bridges (Hashiwokakero) puzzle game.
11
+
12
+ Connect islands with bridges according to the numbers on each island.
13
+ - Each island must have exactly the number of bridges shown
14
+ - Bridges can only run horizontally or vertically
15
+ - Bridges cannot cross each other
16
+ - At most two bridges can connect any pair of islands
17
+ - All islands must be connected in a single network
18
+ """
19
+
20
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
21
+ """Initialize a new Bridges game.
22
+
23
+ Args:
24
+ difficulty: Game difficulty level (easy, medium, hard)
25
+ """
26
+ super().__init__(difficulty, seed, **kwargs)
27
+
28
+ from ...models import DifficultyLevel
29
+
30
+ # Set grid size based on difficulty
31
+ self.size = {
32
+ DifficultyLevel.EASY.value: 7,
33
+ DifficultyLevel.MEDIUM.value: 9,
34
+ DifficultyLevel.HARD.value: 11,
35
+ }.get(self.difficulty.value, 7)
36
+
37
+ # Grid stores island values (0 = water, 1-8 = island with that many bridges needed)
38
+ self.grid: list[list[int]] = [[0 for _ in range(self.size)] for _ in range(self.size)]
39
+
40
+ # Solution stores the bridges (stored as a dict of (r1,c1,r2,c2) -> bridge_count)
41
+ self.solution: dict[tuple[int, int, int, int], int] = {}
42
+
43
+ # Player's current bridges
44
+ self.bridges: dict[tuple[int, int, int, int], int] = {}
45
+
46
+ # Island positions for easy lookup
47
+ self.islands: list[tuple[int, int]] = []
48
+
49
+ @property
50
+ def name(self) -> str:
51
+ """The display name of this puzzle type."""
52
+ return "Bridges"
53
+
54
+ @property
55
+ def description(self) -> str:
56
+ """A one-line description of this puzzle type."""
57
+ return "Connect islands with bridges - satisfy all island numbers"
58
+
59
+ @property
60
+ def constraint_types(self) -> list[str]:
61
+ """Constraint types demonstrated by this puzzle."""
62
+ return ["connectivity", "local_counting", "graph_construction", "path_finding"]
63
+
64
+ @property
65
+ def business_analogies(self) -> list[str]:
66
+ """Business problems this puzzle models."""
67
+ return ["network_design", "infrastructure_planning", "connection_optimization", "graph_connectivity"]
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 = number of bridge connections to place."""
77
+ if not hasattr(self, "solution") or not self.solution:
78
+ return None
79
+ # Each connection is one move, regardless of bridge count (1 or 2)
80
+ return len(self.solution)
81
+
82
+ @property
83
+ def difficulty_profile(self) -> "DifficultyProfile":
84
+ """Difficulty characteristics for Bridges."""
85
+ from ...models import DifficultyLevel
86
+
87
+ logic_depth = {
88
+ DifficultyLevel.EASY.value: 2,
89
+ DifficultyLevel.MEDIUM.value: 4,
90
+ DifficultyLevel.HARD.value: 5,
91
+ }.get(self.difficulty.value, 3)
92
+ return DifficultyProfile(
93
+ logic_depth=logic_depth,
94
+ branching_factor=4.0, # 4 directions, 0-2 bridges
95
+ state_observability=1.0,
96
+ constraint_density=0.5,
97
+ )
98
+
99
+ def _normalize_bridge(self, r1: int, c1: int, r2: int, c2: int) -> tuple[int, int, int, int]:
100
+ """Normalize bridge coordinates so smaller coords come first."""
101
+ if (r1, c1) > (r2, c2):
102
+ return (r2, c2, r1, c1)
103
+ return (r1, c1, r2, c2)
104
+
105
+ def _find_neighbors(self, row: int, col: int) -> list[tuple[int, int]]:
106
+ """Find all islands that can be connected from this position."""
107
+ neighbors = []
108
+
109
+ # Check horizontal (left and right)
110
+ for c in range(col + 1, self.size):
111
+ if self.grid[row][c] > 0:
112
+ neighbors.append((row, c))
113
+ break
114
+
115
+ for c in range(col - 1, -1, -1):
116
+ if self.grid[row][c] > 0:
117
+ neighbors.append((row, c))
118
+ break
119
+
120
+ # Check vertical (up and down)
121
+ for r in range(row + 1, self.size):
122
+ if self.grid[r][col] > 0:
123
+ neighbors.append((r, col))
124
+ break
125
+
126
+ for r in range(row - 1, -1, -1):
127
+ if self.grid[r][col] > 0:
128
+ neighbors.append((r, col))
129
+ break
130
+
131
+ return neighbors
132
+
133
+ def _bridges_cross(self, r1: int, c1: int, r2: int, c2: int, r3: int, c3: int, r4: int, c4: int) -> bool:
134
+ """Check if two bridges would cross each other."""
135
+ # Horizontal bridge 1
136
+ if r1 == r2:
137
+ min_c, max_c = min(c1, c2), max(c1, c2)
138
+ # Vertical bridge 2
139
+ if c3 == c4:
140
+ min_r, max_r = min(r3, r4), max(r3, r4)
141
+ # Check if they intersect
142
+ return min_c < c3 < max_c and min_r < r1 < max_r
143
+
144
+ # Vertical bridge 1
145
+ if c1 == c2:
146
+ min_r, max_r = min(r1, r2), max(r1, r2)
147
+ # Horizontal bridge 2
148
+ if r3 == r4:
149
+ min_c, max_c = min(c3, c4), max(c3, c4)
150
+ # Check if they intersect
151
+ return min_r < r3 < max_r and min_c < c1 < max_c
152
+
153
+ return False
154
+
155
+ async def generate_puzzle(self) -> None:
156
+ """Generate a new Bridges puzzle with retry logic."""
157
+ max_attempts = 50
158
+
159
+ for _attempt in range(max_attempts):
160
+ # Reset state
161
+ self.islands = []
162
+ self.grid = [[0 for _ in range(self.size)] for _ in range(self.size)]
163
+ self.solution = {}
164
+
165
+ # Step 1: Place islands strategically
166
+ if not self._place_islands_strategically():
167
+ continue
168
+
169
+ # Step 2: Generate solution
170
+ self._generate_solution()
171
+
172
+ # Step 3: Validate solution
173
+ if self._validate_puzzle():
174
+ # Step 4: Update island values based on solution
175
+ for r, c in self.islands:
176
+ bridge_count = 0
177
+ for (r1, c1, r2, c2), count in self.solution.items():
178
+ if (r1, c1) == (r, c) or (r2, c2) == (r, c):
179
+ bridge_count += count
180
+ self.grid[r][c] = bridge_count
181
+
182
+ self.game_started = True
183
+ return
184
+
185
+ # Fallback: use last attempt even if not perfect
186
+ for r, c in self.islands:
187
+ bridge_count = 0
188
+ for (r1, c1, r2, c2), count in self.solution.items():
189
+ if (r1, c1) == (r, c) or (r2, c2) == (r, c):
190
+ bridge_count += count
191
+ if bridge_count == 0:
192
+ bridge_count = 1
193
+ self.grid[r][c] = bridge_count
194
+
195
+ self.game_started = True
196
+
197
+ def _place_islands_strategically(self) -> bool:
198
+ """Place islands using strategic positions for better puzzle quality."""
199
+ from ...models import DifficultyLevel
200
+
201
+ num_islands = {
202
+ DifficultyLevel.EASY.value: 8,
203
+ DifficultyLevel.MEDIUM.value: 12,
204
+ DifficultyLevel.HARD.value: 16,
205
+ }.get(self.difficulty.value, 8)
206
+
207
+ # Create a grid of strategic positions (avoid edges for better connectivity)
208
+ step = max(2, self.size // 4)
209
+ strategic_positions = []
210
+
211
+ for r in range(1, self.size - 1, step):
212
+ for c in range(1, self.size - 1, step):
213
+ strategic_positions.append((r, c))
214
+
215
+ # Add some edge positions for variety
216
+ for i in range(1, self.size - 1, step):
217
+ strategic_positions.extend([(0, i), (self.size - 1, i), (i, 0), (i, self.size - 1)])
218
+
219
+ # Shuffle positions for variety
220
+ self._rng.shuffle(strategic_positions)
221
+
222
+ # Select positions ensuring minimum spacing
223
+ min_spacing = 2
224
+ for r, c in strategic_positions:
225
+ if len(self.islands) >= num_islands:
226
+ break
227
+
228
+ # Check spacing from existing islands
229
+ if all(abs(r - ir) + abs(c - ic) >= min_spacing for ir, ic in self.islands):
230
+ self.islands.append((r, c))
231
+ self.grid[r][c] = 1
232
+
233
+ return len(self.islands) >= max(4, num_islands // 2)
234
+
235
+ def _validate_puzzle(self) -> bool:
236
+ """Validate that the generated puzzle is solvable and well-formed."""
237
+ # Check all islands have at least one bridge
238
+ for r, c in self.islands:
239
+ bridge_count = 0
240
+ for (r1, c1, r2, c2), count in self.solution.items():
241
+ if (r1, c1) == (r, c) or (r2, c2) == (r, c):
242
+ bridge_count += count
243
+
244
+ if bridge_count == 0:
245
+ return False
246
+
247
+ # Check bridge count is reasonable (not too high)
248
+ if bridge_count > 8:
249
+ return False
250
+
251
+ # Check all islands are connected (graph connectivity)
252
+ if not self._check_connectivity():
253
+ return False
254
+
255
+ return True
256
+
257
+ def _check_connectivity(self) -> bool:
258
+ """Check if all islands are connected via bridges."""
259
+ if not self.islands:
260
+ return True
261
+
262
+ # BFS from first island
263
+ visited = {self.islands[0]}
264
+ queue = [self.islands[0]]
265
+
266
+ while queue:
267
+ r, c = queue.pop(0)
268
+
269
+ # Check all bridges from this island
270
+ for (r1, c1, r2, c2), count in self.solution.items():
271
+ if count > 0:
272
+ if (r1, c1) == (r, c) and (r2, c2) not in visited:
273
+ visited.add((r2, c2))
274
+ queue.append((r2, c2))
275
+ elif (r2, c2) == (r, c) and (r1, c1) not in visited:
276
+ visited.add((r1, c1))
277
+ queue.append((r1, c1))
278
+
279
+ return len(visited) == len(self.islands)
280
+
281
+ def _would_cross_existing(self, r1: int, c1: int, r2: int, c2: int) -> bool:
282
+ """Check if a new bridge would cross any existing bridge in the solution."""
283
+ for (br1, bc1, br2, bc2), bcount in self.solution.items():
284
+ if bcount > 0:
285
+ if self._bridges_cross(r1, c1, r2, c2, br1, bc1, br2, bc2):
286
+ return True
287
+ return False
288
+
289
+ def _generate_solution(self) -> None:
290
+ """Generate a valid solution for the puzzle without crossing bridges."""
291
+ if not self.islands:
292
+ return
293
+
294
+ # Create a minimum spanning tree using a simple approach
295
+ # but only add bridges that don't cross existing ones
296
+ connected = {self.islands[0]}
297
+ unconnected = set(self.islands[1:])
298
+
299
+ while unconnected:
300
+ # Find the closest unconnected island to any connected island
301
+ # that can be connected without crossing
302
+ best_dist = float("inf")
303
+ best_pair = None
304
+
305
+ for r1, c1 in connected:
306
+ neighbors = self._find_neighbors(r1, c1)
307
+ for r2, c2 in neighbors:
308
+ if (r2, c2) in unconnected:
309
+ # Check if this bridge would cross existing ones
310
+ if not self._would_cross_existing(r1, c1, r2, c2):
311
+ dist = abs(r1 - r2) + abs(c1 - c2)
312
+ if dist < best_dist:
313
+ best_dist = dist
314
+ best_pair = ((r1, c1), (r2, c2))
315
+
316
+ if best_pair:
317
+ (r1, c1), (r2, c2) = best_pair
318
+ bridge_key = self._normalize_bridge(r1, c1, r2, c2)
319
+ self.solution[bridge_key] = 1
320
+ connected.add((r2, c2))
321
+ unconnected.remove((r2, c2))
322
+ else:
323
+ break
324
+
325
+ # Add some additional bridges for variety (up to 2 bridges per connection)
326
+ # Only if they don't create crossings
327
+ for r1, c1 in self.islands:
328
+ neighbors = self._find_neighbors(r1, c1)
329
+ for r2, c2 in neighbors:
330
+ bridge_key = self._normalize_bridge(r1, c1, r2, c2)
331
+ if bridge_key in self.solution and self._rng.random() < 0.3:
332
+ if self.solution[bridge_key] < 2:
333
+ self.solution[bridge_key] += 1
334
+
335
+ async def validate_move(self, *args: Any, **kwargs: Any) -> MoveResult:
336
+ """Validate a bridge placement move.
337
+
338
+ Args:
339
+ args[0]: Starting row (1-indexed)
340
+ args[1]: Starting column (1-indexed)
341
+ args[2]: Ending row (1-indexed)
342
+ args[3]: Ending column (1-indexed)
343
+ args[4]: Number of bridges (1 or 2, or 0 to remove)
344
+
345
+ Returns:
346
+ MoveResult containing success status and message
347
+ """
348
+ if len(args) < 5:
349
+ return MoveResult(success=False, message="Usage: place <row1> <col1> <row2> <col2> <count>")
350
+
351
+ try:
352
+ r1, c1, r2, c2, count = int(args[0]) - 1, int(args[1]) - 1, int(args[2]) - 1, int(args[3]) - 1, int(args[4])
353
+ except (ValueError, IndexError):
354
+ return MoveResult(success=False, message="Invalid coordinates or count")
355
+
356
+ # Validate coordinates
357
+ if not (0 <= r1 < self.size and 0 <= c1 < self.size and 0 <= r2 < self.size and 0 <= c2 < self.size):
358
+ return MoveResult(success=False, message="Coordinates out of range")
359
+
360
+ # Check that they're not the same position (before checking if it's an island)
361
+ if r1 == r2 and c1 == c2:
362
+ return MoveResult(success=False, message="Cannot connect island to itself")
363
+
364
+ # Check that both positions are islands
365
+ if self.grid[r1][c1] == 0 or self.grid[r2][c2] == 0:
366
+ return MoveResult(success=False, message="Both positions must be islands")
367
+
368
+ # Check that islands are in a line (horizontal or vertical)
369
+ if r1 != r2 and c1 != c2:
370
+ return MoveResult(success=False, message="Bridges must be horizontal or vertical")
371
+
372
+ # Normalize bridge coordinates
373
+ bridge_key = self._normalize_bridge(r1, c1, r2, c2)
374
+
375
+ # Validate count
376
+ if count < 0 or count > 2:
377
+ return MoveResult(success=False, message="Bridge count must be 0, 1, or 2")
378
+
379
+ # Check for crossing bridges
380
+ if count > 0:
381
+ for (br1, bc1, br2, bc2), bcount in self.bridges.items():
382
+ if bcount > 0 and (br1, bc1, br2, bc2) != bridge_key:
383
+ if self._bridges_cross(r1, c1, r2, c2, br1, bc1, br2, bc2):
384
+ return MoveResult(success=False, message="Bridges cannot cross")
385
+
386
+ # Update or remove bridge
387
+ if count == 0:
388
+ if bridge_key in self.bridges:
389
+ del self.bridges[bridge_key]
390
+ self.moves_made += 1
391
+ return MoveResult(success=True, message="Bridge removed")
392
+ return MoveResult(success=False, message="No bridge to remove")
393
+ else:
394
+ self.bridges[bridge_key] = count
395
+ self.moves_made += 1
396
+ return MoveResult(success=True, message=f"Bridge placed ({count} bridge{'s' if count > 1 else ''})")
397
+
398
+ def is_complete(self) -> bool:
399
+ """Check if the puzzle is completely and correctly solved."""
400
+ # Check that each island has the correct number of bridges
401
+ for r, c in self.islands:
402
+ required = self.grid[r][c]
403
+ actual = 0
404
+
405
+ for (r1, c1, r2, c2), count in self.bridges.items():
406
+ if (r1, c1) == (r, c) or (r2, c2) == (r, c):
407
+ actual += count
408
+
409
+ if actual != required:
410
+ return False
411
+
412
+ # Check that all islands are connected (simplified check)
413
+ # In a full implementation, we'd do a proper connectivity check
414
+ return len(self.bridges) >= len(self.islands) - 1
415
+
416
+ async def get_hint(self) -> tuple[Any, str] | None:
417
+ """Get a hint for the next move."""
418
+ # Find a bridge in the solution that's not yet placed correctly
419
+ for bridge_key, solution_count in self.solution.items():
420
+ current_count = self.bridges.get(bridge_key, 0)
421
+ if current_count != solution_count:
422
+ r1, c1, r2, c2 = bridge_key
423
+ return (
424
+ (r1 + 1, c1 + 1, r2 + 1, c2 + 1, solution_count),
425
+ f"Try placing {solution_count} bridge(s) between ({r1 + 1},{c1 + 1}) and ({r2 + 1},{c2 + 1})",
426
+ )
427
+
428
+ return None
429
+
430
+ def render_grid(self) -> str:
431
+ """Render the current puzzle state as ASCII art."""
432
+ lines = []
433
+
434
+ # Header
435
+ header = " |"
436
+ for c in range(self.size):
437
+ header += f" {c + 1:2d}"
438
+ lines.append(header)
439
+ lines.append(" +" + "---" * self.size)
440
+
441
+ # Grid rows
442
+ for r in range(self.size):
443
+ row = f"{r + 1:2d}|"
444
+ for c in range(self.size):
445
+ if self.grid[r][c] > 0:
446
+ # This is an island
447
+ row += f" {self.grid[r][c]:2d}"
448
+ else:
449
+ # Check for bridges
450
+ bridge_char = " ."
451
+
452
+ # Check horizontal bridges
453
+ for (r1, c1, r2, c2), count in self.bridges.items():
454
+ if r1 == r2 == r and min(c1, c2) < c < max(c1, c2):
455
+ bridge_char = " ══" if count == 2 else " ──"
456
+ break
457
+
458
+ # Check vertical bridges
459
+ for (r1, c1, r2, c2), count in self.bridges.items():
460
+ if c1 == c2 == c and min(r1, r2) < r < max(r1, r2):
461
+ bridge_char = " ║ " if count == 2 else " │ "
462
+ break
463
+
464
+ row += bridge_char
465
+
466
+ lines.append(row)
467
+
468
+ return "\n".join(lines)
469
+
470
+ def get_rules(self) -> str:
471
+ """Get the rules description for this puzzle type."""
472
+ return """BRIDGES (HASHIWOKAKERO) RULES:
473
+ - Connect all islands with bridges
474
+ - Each island shows the number of bridges it needs
475
+ - Bridges can only be horizontal or vertical
476
+ - Bridges cannot cross each other
477
+ - At most 2 bridges can connect any pair of islands
478
+ - All islands must be connected in a single network"""
479
+
480
+ def get_commands(self) -> str:
481
+ """Get the available commands for this puzzle type."""
482
+ return """BRIDGES COMMANDS:
483
+ place <r1> <c1> <r2> <c2> <count> - Place bridges (1 or 2, or 0 to remove)
484
+ Example: place 1 1 1 3 2 (places 2 bridges between islands)
485
+ hint - Get a hint for the next move
486
+ check - Check if puzzle is complete
487
+ solve - Show the solution
488
+ menu - Return to main menu
489
+ quit - Exit the game"""
@@ -0,0 +1,6 @@
1
+ """Einstein puzzle game module."""
2
+
3
+ from .config import EinsteinConfig
4
+ from .game import EinsteinGame
5
+
6
+ __all__ = ["EinsteinGame", "EinsteinConfig"]
@@ -0,0 +1,23 @@
1
+ """Configuration for Einstein game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class EinsteinConfig(BaseModel):
9
+ """Configuration for Einstein puzzle game."""
10
+
11
+ difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
12
+ num_clues: int = Field(ge=5, le=15, description="Number of clues provided")
13
+
14
+ @classmethod
15
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "EinsteinConfig":
16
+ """Create config from difficulty level."""
17
+ config_map = {
18
+ DifficultyLevel.EASY: {"num_clues": 12},
19
+ DifficultyLevel.MEDIUM: {"num_clues": 10},
20
+ DifficultyLevel.HARD: {"num_clues": 8},
21
+ }
22
+ params = config_map[difficulty]
23
+ return cls(difficulty=difficulty, **params)
@@ -0,0 +1,13 @@
1
+ """Einstein's Puzzle constants and static data."""
2
+
3
+ from typing import Final
4
+
5
+ # Einstein's Puzzle attributes
6
+ COLORS: Final[list[str]] = ["Red", "Green", "Blue", "Yellow", "White"]
7
+ NATIONALITIES: Final[list[str]] = ["British", "Swedish", "Danish", "Norwegian", "German"]
8
+ DRINKS: Final[list[str]] = ["Tea", "Coffee", "Milk", "Beer", "Water"]
9
+ SMOKES: Final[list[str]] = ["Pall Mall", "Dunhill", "Blend", "Blue Master", "Prince"]
10
+ PETS: Final[list[str]] = ["Dog", "Bird", "Cat", "Horse", "Fish"]
11
+
12
+ # Attribute names (for iteration)
13
+ ATTRIBUTES: Final[list[str]] = ["color", "nationality", "drink", "smoke", "pet"]