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,434 @@
1
+ """Binary Puzzle game implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ...models import DifficultyLevel, DifficultyProfile, MoveResult
6
+ from .._base import PuzzleGame
7
+ from .config import BinaryConfig
8
+
9
+
10
+ class BinaryPuzzleGame(PuzzleGame):
11
+ """Binary Puzzle (also known as Takuzu or Binairo).
12
+
13
+ Fill a grid with 0s and 1s following these rules:
14
+ - No more than two consecutive 0s or 1s in any row or column
15
+ - Each row and column must have equal numbers of 0s and 1s
16
+ - No two rows are identical, no two columns are identical
17
+ """
18
+
19
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
20
+ """Initialize a new Binary Puzzle game.
21
+
22
+ Args:
23
+ difficulty: Game difficulty level (easy=6x6, medium=8x8, hard=10x10)
24
+ """
25
+ super().__init__(difficulty, seed, **kwargs)
26
+
27
+ # Grid size based on difficulty (must be even)
28
+ self.config = BinaryConfig.from_difficulty(self.difficulty)
29
+ self.size = self.config.size
30
+
31
+ # Grid: -1 = empty, 0 or 1 = filled
32
+ self.grid = [[-1 for _ in range(self.size)] for _ in range(self.size)]
33
+ self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
34
+ self.initial_grid = [[-1 for _ in range(self.size)] for _ in range(self.size)]
35
+
36
+ @property
37
+ def name(self) -> str:
38
+ """The display name of this puzzle type."""
39
+ return "Binary Puzzle"
40
+
41
+ @property
42
+ def description(self) -> str:
43
+ """A one-line description of this puzzle type."""
44
+ return "Fill grid with 0s and 1s - no three in a row, equal counts"
45
+
46
+ @property
47
+ def constraint_types(self) -> list[str]:
48
+ """Constraint types demonstrated by this puzzle."""
49
+ return ["all_different", "no_three_consecutive", "equal_counts", "pattern_avoidance"]
50
+
51
+ @property
52
+ def business_analogies(self) -> list[str]:
53
+ """Business problems this puzzle models."""
54
+ return ["binary_allocation", "balanced_distribution", "pattern_constraints", "quota_management"]
55
+
56
+ @property
57
+ def complexity_profile(self) -> dict[str, str]:
58
+ """Complexity profile of this puzzle."""
59
+ return {"reasoning_type": "deductive", "search_space": "medium", "constraint_density": "moderate"}
60
+
61
+ @property
62
+ def optimal_steps(self) -> int | None:
63
+ """Minimum steps = empty cells to fill."""
64
+ return sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1)
65
+
66
+ @property
67
+ def difficulty_profile(self) -> "DifficultyProfile":
68
+ """Difficulty characteristics for Binary Puzzle."""
69
+
70
+ empty = self.optimal_steps or 0
71
+ total = self.size * self.size
72
+ logic_depth = {
73
+ DifficultyLevel.EASY.value: 2,
74
+ DifficultyLevel.MEDIUM.value: 3,
75
+ DifficultyLevel.HARD.value: 4,
76
+ }.get(self.difficulty.value, 2)
77
+ branching = 2.0 # Binary choice per cell
78
+ density = 1.0 - (empty / total) if total > 0 else 0.5
79
+ return DifficultyProfile(
80
+ logic_depth=logic_depth,
81
+ branching_factor=branching,
82
+ state_observability=1.0,
83
+ constraint_density=round(density, 2),
84
+ )
85
+
86
+ def _check_no_three_consecutive(self, grid: list[list[int]]) -> bool:
87
+ """Check if there are no three consecutive 0s or 1s.
88
+
89
+ Args:
90
+ grid: The grid to check
91
+
92
+ Returns:
93
+ True if valid, False if three consecutive found
94
+ """
95
+ # Check rows
96
+ for row in range(self.size):
97
+ for col in range(self.size - 2):
98
+ vals = [grid[row][col], grid[row][col + 1], grid[row][col + 2]]
99
+ if -1 not in vals and len(set(vals)) == 1:
100
+ return False
101
+
102
+ # Check columns
103
+ for col in range(self.size):
104
+ for row in range(self.size - 2):
105
+ vals = [grid[row][col], grid[row + 1][col], grid[row + 2][col]]
106
+ if -1 not in vals and len(set(vals)) == 1:
107
+ return False
108
+
109
+ return True
110
+
111
+ def _check_equal_counts(self, sequence: list[int]) -> bool:
112
+ """Check if a completed sequence has equal 0s and 1s.
113
+
114
+ Args:
115
+ sequence: List of values
116
+
117
+ Returns:
118
+ True if equal counts or incomplete, False if counts are wrong
119
+ """
120
+ if -1 in sequence:
121
+ # Not complete yet
122
+ count_0 = sequence.count(0)
123
+ count_1 = sequence.count(1)
124
+ # Check if we haven't exceeded the limit
125
+ return count_0 <= self.size // 2 and count_1 <= self.size // 2
126
+
127
+ return sequence.count(0) == sequence.count(1) == self.size // 2
128
+
129
+ def _generate_valid_solution(self) -> bool:
130
+ """Generate a valid binary puzzle solution using backtracking.
131
+
132
+ Returns:
133
+ True if solution generated successfully
134
+ """
135
+ for row in range(self.size):
136
+ for col in range(self.size):
137
+ if self.solution[row][col] == -1:
138
+ # Try 0 and 1
139
+ for value in [0, 1]:
140
+ self.solution[row][col] = value
141
+
142
+ # Check constraints
143
+ if self._check_no_three_consecutive(self.solution):
144
+ # Check row count constraint
145
+ row_vals = self.solution[row]
146
+ if self._check_equal_counts(row_vals):
147
+ # Check column count constraint
148
+ col_vals = [self.solution[r][col] for r in range(self.size)]
149
+ if self._check_equal_counts(col_vals):
150
+ if self._generate_valid_solution():
151
+ return True
152
+
153
+ self.solution[row][col] = -1
154
+
155
+ return False
156
+
157
+ return True
158
+
159
+ def _verify_binary_solution(self) -> bool:
160
+ """Verify the solution satisfies all binary puzzle constraints."""
161
+ # Check all rows
162
+ for row in range(self.size):
163
+ row_vals = self.solution[row]
164
+ if row_vals.count(0) != self.size // 2 or row_vals.count(1) != self.size // 2:
165
+ return False
166
+ # Check no three consecutive
167
+ for col in range(self.size - 2):
168
+ if row_vals[col] == row_vals[col + 1] == row_vals[col + 2]:
169
+ return False
170
+
171
+ # Check all columns
172
+ for col in range(self.size):
173
+ col_vals = [self.solution[r][col] for r in range(self.size)]
174
+ if col_vals.count(0) != self.size // 2 or col_vals.count(1) != self.size // 2:
175
+ return False
176
+ # Check no three consecutive
177
+ for row in range(self.size - 2):
178
+ if col_vals[row] == col_vals[row + 1] == col_vals[row + 2]:
179
+ return False
180
+
181
+ return True
182
+
183
+ def _create_fallback_binary_solution(self) -> None:
184
+ """Create a simple valid binary solution using alternating pattern."""
185
+ # Use a known valid pattern: alternating 0011 style
186
+ self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
187
+
188
+ for row in range(self.size):
189
+ for col in range(self.size):
190
+ # Create a pattern that avoids three consecutive
191
+ # Pattern: 0,0,1,1,0,0 for even rows, 1,1,0,0,1,1 for odd rows
192
+ block = (col // 2) % 2
193
+ if row % 2 == 1:
194
+ block = 1 - block
195
+ self.solution[row][col] = block
196
+
197
+ def _generate_valid_binary_row(self, row: int) -> list[int] | None:
198
+ """Generate a valid row that satisfies all constraints.
199
+
200
+ Args:
201
+ row: Row index to generate
202
+
203
+ Returns:
204
+ Valid pattern or None if generation fails
205
+ """
206
+ # Create pattern with equal 0s and 1s
207
+ pattern = [0] * (self.size // 2) + [1] * (self.size // 2)
208
+
209
+ for _ in range(200):
210
+ self._rng.shuffle(pattern)
211
+
212
+ # Check this row doesn't have three consecutive
213
+ has_three = False
214
+ for col in range(self.size - 2):
215
+ if pattern[col] == pattern[col + 1] == pattern[col + 2]:
216
+ has_three = True
217
+ break
218
+
219
+ if has_three:
220
+ continue
221
+
222
+ # Check column constraints so far
223
+ valid = True
224
+ for col in range(self.size):
225
+ col_vals = [self.solution[r][col] for r in range(row)] + [pattern[col]]
226
+ if col_vals.count(0) > self.size // 2 or col_vals.count(1) > self.size // 2:
227
+ valid = False
228
+ break
229
+
230
+ # Check no three consecutive in column
231
+ if row >= 2:
232
+ if pattern[col] == self.solution[row - 1][col] == self.solution[row - 2][col]:
233
+ valid = False
234
+ break
235
+
236
+ if valid:
237
+ return pattern[:]
238
+
239
+ return None
240
+
241
+ async def generate_puzzle(self) -> None:
242
+ """Generate a new Binary Puzzle."""
243
+ max_restarts = 20
244
+
245
+ for _ in range(max_restarts):
246
+ # Start with empty solution
247
+ self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
248
+
249
+ # Try to generate each row
250
+ success = True
251
+ for row in range(self.size):
252
+ pattern = self._generate_valid_binary_row(row)
253
+ if pattern is None:
254
+ success = False
255
+ break
256
+ self.solution[row] = pattern
257
+
258
+ if success:
259
+ # Verify the solution
260
+ if self._verify_binary_solution():
261
+ break
262
+ else:
263
+ # Fallback: use a simple alternating pattern
264
+ self._create_fallback_binary_solution()
265
+
266
+ # Remove some cells based on difficulty
267
+ cells_to_remove_map = {
268
+ DifficultyLevel.EASY: self.size * 2,
269
+ DifficultyLevel.MEDIUM: self.size * 3,
270
+ DifficultyLevel.HARD: self.size * 4,
271
+ }
272
+ cells_to_remove = cells_to_remove_map[self.difficulty]
273
+
274
+ # Copy solution to grid
275
+ self.grid = [row[:] for row in self.solution]
276
+
277
+ # Randomly remove cells
278
+ cells = [(r, c) for r in range(self.size) for c in range(self.size)]
279
+ self._rng.shuffle(cells)
280
+
281
+ for r, c in cells[:cells_to_remove]:
282
+ self.grid[r][c] = -1
283
+
284
+ self.initial_grid = [row[:] for row in self.grid]
285
+ self.moves_made = 0
286
+ self.game_started = True
287
+
288
+ async def validate_move(self, row: int, col: int, num: int) -> MoveResult:
289
+ """Place a number on the grid.
290
+
291
+ Args:
292
+ row: Row index (1-indexed, user-facing)
293
+ col: Column index (1-indexed, user-facing)
294
+ num: Number to place (0, 1, or -1 to clear)
295
+
296
+ Returns:
297
+ MoveResult indicating success/failure and message
298
+ """
299
+ # Convert to 0-indexed
300
+ row -= 1
301
+ col -= 1
302
+
303
+ # Validate coordinates
304
+ if not (0 <= row < self.size and 0 <= col < self.size):
305
+ return MoveResult(success=False, message=f"Invalid coordinates. Use row and column between 1-{self.size}.")
306
+
307
+ # Check if this cell is part of the initial puzzle
308
+ if self.initial_grid[row][col] != -1:
309
+ return MoveResult(success=False, message="Cannot modify initial puzzle cells.")
310
+
311
+ # Clear the cell
312
+ if num == -1 or num == 2: # Accept 2 as clear command for convenience
313
+ self.grid[row][col] = -1
314
+ return MoveResult(success=True, message="Cell cleared.", state_changed=True)
315
+
316
+ # Validate number
317
+ if num not in [0, 1]:
318
+ return MoveResult(success=False, message="Invalid number. Use 0, 1, or 2 to clear.")
319
+
320
+ # Check if the move is valid
321
+ old_value = self.grid[row][col]
322
+ self.grid[row][col] = num
323
+
324
+ # Check no three consecutive
325
+ if not self._check_no_three_consecutive(self.grid):
326
+ self.grid[row][col] = old_value
327
+ return MoveResult(success=False, message="Invalid move! This creates three consecutive identical values.")
328
+
329
+ # Check count constraints
330
+ row_vals = self.grid[row]
331
+ if not self._check_equal_counts(row_vals):
332
+ self.grid[row][col] = old_value
333
+ return MoveResult(success=False, message="Invalid move! This exceeds the count limit for this row.")
334
+
335
+ col_vals = [self.grid[r][col] for r in range(self.size)]
336
+ if not self._check_equal_counts(col_vals):
337
+ self.grid[row][col] = old_value
338
+ return MoveResult(success=False, message="Invalid move! This exceeds the count limit for this column.")
339
+
340
+ self.moves_made += 1
341
+ return MoveResult(success=True, message="Number placed successfully!", state_changed=True)
342
+
343
+ def is_complete(self) -> bool:
344
+ """Check if the puzzle is complete and correct."""
345
+ # Check all cells filled
346
+ for row in range(self.size):
347
+ for col in range(self.size):
348
+ if self.grid[row][col] == -1:
349
+ return False
350
+ if self.grid[row][col] != self.solution[row][col]:
351
+ return False
352
+
353
+ return True
354
+
355
+ async def get_hint(self) -> tuple[Any, str] | None:
356
+ """Get a hint for the next move.
357
+
358
+ Returns:
359
+ Tuple of (hint_data, hint_message) or None if puzzle is complete
360
+ """
361
+ empty_cells = [(r, c) for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1]
362
+ if not empty_cells:
363
+ return None
364
+
365
+ row, col = self._rng.choice(empty_cells)
366
+ hint_data = (row + 1, col + 1, self.solution[row][col])
367
+ hint_message = f"Try placing {self.solution[row][col]} at row {row + 1}, column {col + 1}"
368
+ return hint_data, hint_message
369
+
370
+ def render_grid(self) -> str:
371
+ """Render the current puzzle state as ASCII art.
372
+
373
+ Returns:
374
+ String representation of the puzzle grid
375
+ """
376
+ lines = []
377
+
378
+ # Header - align with row format "NN|"
379
+ header = " |"
380
+ for i in range(self.size):
381
+ col_label = str(i + 1) if i < 9 else chr(65 + i - 9)
382
+ header += f"{col_label}|"
383
+ lines.append(header)
384
+ lines.append(" +" + "-+" * self.size)
385
+
386
+ for row in range(self.size):
387
+ line = f"{row + 1:2d}|"
388
+ for col in range(self.size):
389
+ cell = self.grid[row][col]
390
+ if cell == -1:
391
+ line += ".|"
392
+ else:
393
+ line += f"{cell}|"
394
+ lines.append(line)
395
+ lines.append(" +" + "-+" * self.size)
396
+
397
+ return "\n".join(lines)
398
+
399
+ def get_rules(self) -> str:
400
+ """Get the rules description for Binary Puzzle.
401
+
402
+ Returns:
403
+ Multi-line string describing the puzzle rules
404
+ """
405
+ return f"""BINARY PUZZLE RULES:
406
+ - Fill {self.size}x{self.size} grid with 0s and 1s
407
+ - Max 2 consecutive 0s or 1s per row/column
408
+ - Each row/column: {self.size // 2} zeros, {self.size // 2} ones
409
+ - All rows unique, all columns unique"""
410
+
411
+ def get_commands(self) -> str:
412
+ """Get the available commands for Binary Puzzle.
413
+
414
+ Returns:
415
+ Multi-line string describing available commands
416
+ """
417
+ return """BINARY PUZZLE COMMANDS:
418
+ place <row> <col> <num> - Place 0 or 1 (e.g., 'place 1 2 0')
419
+ clear <row> <col> - Clear a cell (or use 'place <row> <col> 2')
420
+ show - Display the current grid
421
+ hint - Get a hint for the next move
422
+ check - Check your progress
423
+ solve - Show the solution (ends game)
424
+ menu - Return to game selection
425
+ quit - Exit the server"""
426
+
427
+ def get_stats(self) -> str:
428
+ """Get current game statistics.
429
+
430
+ Returns:
431
+ String with game stats
432
+ """
433
+ empty = sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1)
434
+ return f"Moves made: {self.moves_made} | Empty cells: {empty} | Grid size: {self.size}x{self.size} | Seed: {self.seed}"
@@ -0,0 +1,6 @@
1
+ """Bridges puzzle game module."""
2
+
3
+ from .config import BridgesConfig
4
+ from .game import BridgesGame
5
+
6
+ __all__ = ["BridgesGame", "BridgesConfig"]
@@ -0,0 +1,24 @@
1
+ """Configuration for Bridges game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class BridgesConfig(BaseModel):
9
+ """Configuration for Bridges 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
+ num_islands: int = Field(ge=4, description="Number of islands")
14
+
15
+ @classmethod
16
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "BridgesConfig":
17
+ """Create config from difficulty level."""
18
+ config_map = {
19
+ DifficultyLevel.EASY: {"size": 5, "num_islands": 5},
20
+ DifficultyLevel.MEDIUM: {"size": 7, "num_islands": 8},
21
+ DifficultyLevel.HARD: {"size": 9, "num_islands": 12},
22
+ }
23
+ params = config_map[difficulty]
24
+ return cls(difficulty=difficulty, **params)