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,328 @@
1
+ """Sudoku puzzle game implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ...models import DifficultyProfile, MoveResult
6
+ from .._base import PuzzleGame
7
+ from .config import SudokuConfig
8
+
9
+
10
+ class SudokuGame(PuzzleGame):
11
+ """Classic 9x9 Sudoku puzzle game."""
12
+
13
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
14
+ """Initialize a new Sudoku game.
15
+
16
+ Args:
17
+ difficulty: Game difficulty level (easy, medium, hard)
18
+ seed: Random seed for reproducible puzzle generation
19
+ """
20
+ super().__init__(difficulty, seed, **kwargs)
21
+ self.config = SudokuConfig.from_difficulty(self.difficulty)
22
+ self.grid = [[0 for _ in range(9)] for _ in range(9)]
23
+ self.solution = [[0 for _ in range(9)] for _ in range(9)]
24
+ self.initial_grid = [[0 for _ in range(9)] for _ in range(9)]
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ """The display name of this puzzle type."""
29
+ return "Sudoku"
30
+
31
+ @property
32
+ def description(self) -> str:
33
+ """A one-line description of this puzzle type."""
34
+ return "Classic logic puzzle - fill 9x9 grid with digits 1-9"
35
+
36
+ @property
37
+ def constraint_types(self) -> list[str]:
38
+ """Constraint types demonstrated by this puzzle."""
39
+ return ["all_different", "regional_uniqueness", "grid_constraints", "multi_level_constraints"]
40
+
41
+ @property
42
+ def business_analogies(self) -> list[str]:
43
+ """Business problems this puzzle models."""
44
+ return ["resource_assignment", "unique_allocation", "multi_constraint_satisfaction", "grid_scheduling"]
45
+
46
+ @property
47
+ def complexity_profile(self) -> dict[str, str]:
48
+ """Complexity profile of this puzzle."""
49
+ return {"reasoning_type": "deductive", "search_space": "large", "constraint_density": "moderate"}
50
+
51
+ @property
52
+ def complexity_metrics(self) -> dict[str, int | float]:
53
+ """Quantified complexity metrics for this Sudoku puzzle."""
54
+ empty_cells = sum(1 for r in range(9) for c in range(9) if self.grid[r][c] == 0)
55
+ # Sudoku has 27 AllDifferent constraints: 9 rows + 9 cols + 9 boxes
56
+ constraint_count = 27
57
+ # 81 cells total, domain is 1-9
58
+ variable_count = 81
59
+ domain_size = 9
60
+ # Branching factor depends on how constrained each cell is
61
+ # For a well-formed puzzle, average is around 2-3
62
+ branching_factor = 2.5 if empty_cells > 0 else 0.0
63
+ return {
64
+ "variable_count": variable_count,
65
+ "constraint_count": constraint_count,
66
+ "domain_size": domain_size,
67
+ "branching_factor": branching_factor,
68
+ "empty_cells": empty_cells,
69
+ }
70
+
71
+ @property
72
+ def optimal_steps(self) -> int | None:
73
+ """Minimum steps to solve = number of empty cells to fill."""
74
+ return sum(1 for r in range(9) for c in range(9) if self.grid[r][c] == 0)
75
+
76
+ @property
77
+ def difficulty_profile(self) -> "DifficultyProfile":
78
+ """Detailed difficulty characteristics for Sudoku."""
79
+ from ...models import DifficultyLevel
80
+
81
+ empty = self.optimal_steps or 0
82
+ # Logic depth: easy puzzles need simple elimination, hard need chains
83
+ logic_depth = {
84
+ DifficultyLevel.EASY.value: 2,
85
+ DifficultyLevel.MEDIUM.value: 4,
86
+ DifficultyLevel.HARD.value: 6,
87
+ }.get(self.difficulty.value, 3)
88
+ # Branching factor increases with empty cells
89
+ branching = 2.0 + (empty / 81) * 4 # 2-6 range
90
+ # Constraint density is inverse of empty cells ratio
91
+ density = 1.0 - (empty / 81)
92
+
93
+ return DifficultyProfile(
94
+ logic_depth=logic_depth,
95
+ branching_factor=round(branching, 1),
96
+ state_observability=1.0,
97
+ constraint_density=round(density, 2),
98
+ )
99
+
100
+ def is_valid_move(self, row: int, col: int, num: int, grid: list[list[int]] | None = None) -> bool:
101
+ """Check if placing num at (row, col) is valid according to sudoku rules.
102
+
103
+ Args:
104
+ row: Row index (0-8)
105
+ col: Column index (0-8)
106
+ num: Number to place (1-9)
107
+ grid: Grid to check against (defaults to self.grid)
108
+
109
+ Returns:
110
+ True if the move is valid, False otherwise
111
+ """
112
+ if grid is None:
113
+ grid = self.grid
114
+
115
+ # Check row
116
+ for c in range(9):
117
+ if c != col and grid[row][c] == num:
118
+ return False
119
+
120
+ # Check column
121
+ for r in range(9):
122
+ if r != row and grid[r][col] == num:
123
+ return False
124
+
125
+ # Check 3x3 box
126
+ box_row, box_col = 3 * (row // 3), 3 * (col // 3)
127
+ for r in range(box_row, box_row + 3):
128
+ for c in range(box_col, box_col + 3):
129
+ if (r != row or c != col) and grid[r][c] == num:
130
+ return False
131
+
132
+ return True
133
+
134
+ def solve(self, grid: list[list[int]]) -> bool:
135
+ """Solve the sudoku puzzle using backtracking.
136
+
137
+ Args:
138
+ grid: The sudoku grid to solve
139
+
140
+ Returns:
141
+ True if solved, False otherwise
142
+ """
143
+ for row in range(9):
144
+ for col in range(9):
145
+ if grid[row][col] == 0:
146
+ for num in range(1, 10):
147
+ # Temporarily place the number
148
+ grid[row][col] = num
149
+
150
+ # Check if it's valid (check against the grid being solved)
151
+ if self.is_valid_move(row, col, num, grid) and self.solve(grid):
152
+ return True
153
+
154
+ # Backtrack
155
+ grid[row][col] = 0
156
+
157
+ return False
158
+ return True
159
+
160
+ async def generate_puzzle(self) -> None:
161
+ """Generate a new sudoku puzzle."""
162
+ # Start with an empty grid
163
+ self.grid = [[0 for _ in range(9)] for _ in range(9)]
164
+
165
+ # Fill diagonal 3x3 boxes (they don't interfere with each other)
166
+ for box in range(3):
167
+ nums = list(range(1, 10))
168
+ self._rng.shuffle(nums)
169
+ for i in range(3):
170
+ for j in range(3):
171
+ self.grid[box * 3 + i][box * 3 + j] = nums[i * 3 + j]
172
+
173
+ # Solve the complete grid
174
+ self.solution = [row[:] for row in self.grid]
175
+ self.solve(self.solution)
176
+ self.grid = [row[:] for row in self.solution]
177
+
178
+ # Remove numbers based on difficulty
179
+ cells_to_remove = self.config.cells_to_remove
180
+
181
+ # Randomly remove numbers
182
+ cells = [(r, c) for r in range(9) for c in range(9)]
183
+ self._rng.shuffle(cells)
184
+
185
+ for r, c in cells[:cells_to_remove]:
186
+ self.grid[r][c] = 0
187
+
188
+ # Store the initial state
189
+ self.initial_grid = [row[:] for row in self.grid]
190
+ self.moves_made = 0
191
+ self.game_started = True
192
+
193
+ async def validate_move(self, row: int, col: int, num: int) -> MoveResult:
194
+ """Place a number on the grid.
195
+
196
+ Args:
197
+ row: Row index (1-9, user-facing)
198
+ col: Column index (1-9, user-facing)
199
+ num: Number to place (1-9, or 0 to clear)
200
+
201
+ Returns:
202
+ MoveResult with success status and message
203
+ """
204
+ # Convert to 0-indexed
205
+ row -= 1
206
+ col -= 1
207
+
208
+ # Validate coordinates
209
+ if not (0 <= row < 9 and 0 <= col < 9):
210
+ return MoveResult(success=False, message="Invalid coordinates. Use row and column between 1-9.")
211
+
212
+ # Check if this cell is part of the initial puzzle
213
+ if self.initial_grid[row][col] != 0:
214
+ return MoveResult(success=False, message="Cannot modify initial puzzle cells.")
215
+
216
+ # Clear the cell
217
+ if num == 0:
218
+ self.grid[row][col] = 0
219
+ return MoveResult(success=True, message="Cell cleared.", state_changed=True)
220
+
221
+ # Validate number
222
+ if not (1 <= num <= 9):
223
+ return MoveResult(success=False, message="Invalid number. Use 1-9 or 0 to clear.")
224
+
225
+ # Check if the move is valid
226
+ old_value = self.grid[row][col]
227
+ self.grid[row][col] = num
228
+
229
+ if not self.is_valid_move(row, col, num):
230
+ self.grid[row][col] = old_value
231
+ return MoveResult(success=False, message="Invalid move! This number conflicts with sudoku rules.")
232
+
233
+ self.moves_made += 1
234
+ return MoveResult(success=True, message="Number placed successfully!", state_changed=True)
235
+
236
+ def is_complete(self) -> bool:
237
+ """Check if the puzzle is complete and correct."""
238
+ for row in range(9):
239
+ for col in range(9):
240
+ if self.grid[row][col] == 0:
241
+ return False
242
+ if self.grid[row][col] != self.solution[row][col]:
243
+ return False
244
+ return True
245
+
246
+ async def get_hint(self) -> tuple[Any, str] | None:
247
+ """Get a hint for the next move.
248
+
249
+ Returns:
250
+ Tuple of (hint_data, hint_message) or None if puzzle is complete
251
+ """
252
+ empty_cells = [(r, c) for r in range(9) for c in range(9) if self.grid[r][c] == 0]
253
+ if not empty_cells:
254
+ return None
255
+
256
+ row, col = self._rng.choice(empty_cells)
257
+ hint_data = (row + 1, col + 1, self.solution[row][col])
258
+ hint_message = f"Try placing {self.solution[row][col]} at row {row + 1}, column {col + 1}"
259
+ return hint_data, hint_message
260
+
261
+ def render_grid(self) -> str:
262
+ """Render the current puzzle state as ASCII art.
263
+
264
+ Returns:
265
+ String representation of the puzzle grid
266
+ """
267
+ lines = []
268
+ lines.append(" | 1 2 3 | 4 5 6 | 7 8 9 |")
269
+ lines.append(" " + "-" * 25)
270
+
271
+ for row in range(9):
272
+ if row > 0 and row % 3 == 0:
273
+ lines.append(" " + "-" * 25)
274
+
275
+ line = f"{row + 1} |"
276
+ for col in range(9):
277
+ if col > 0 and col % 3 == 0:
278
+ line += " |"
279
+
280
+ cell = self.grid[row][col]
281
+ if cell == 0:
282
+ line += " ."
283
+ else:
284
+ line += f" {cell}"
285
+
286
+ line += " |"
287
+ lines.append(line)
288
+
289
+ lines.append(" " + "-" * 25)
290
+ return "\n".join(lines)
291
+
292
+ def get_rules(self) -> str:
293
+ """Get the rules description for Sudoku.
294
+
295
+ Returns:
296
+ Multi-line string describing the puzzle rules
297
+ """
298
+ return """SUDOKU RULES:
299
+ - Fill the 9x9 grid with numbers 1-9
300
+ - Each row must contain 1-9 without repeats
301
+ - Each column must contain 1-9 without repeats
302
+ - Each 3x3 box must contain 1-9 without repeats
303
+ - Some cells are pre-filled and cannot be modified"""
304
+
305
+ def get_commands(self) -> str:
306
+ """Get the available commands for Sudoku.
307
+
308
+ Returns:
309
+ Multi-line string describing available commands
310
+ """
311
+ return """SUDOKU COMMANDS:
312
+ place <row> <col> <num> - Place a number (e.g., 'place 1 5 7')
313
+ clear <row> <col> - Clear a cell you've filled
314
+ show - Display the current grid
315
+ hint - Get a hint for the next move
316
+ check - Check your progress
317
+ solve - Show the solution (ends game)
318
+ menu - Return to game selection
319
+ quit - Exit the server"""
320
+
321
+ def get_stats(self) -> str:
322
+ """Get current game statistics.
323
+
324
+ Returns:
325
+ String with game stats
326
+ """
327
+ empty = sum(1 for r in range(9) for c in range(9) if self.grid[r][c] == 0)
328
+ return f"Moves made: {self.moves_made} | Empty cells: {empty} | Seed: {self.seed}"
@@ -0,0 +1,6 @@
1
+ """Tents puzzle game module."""
2
+
3
+ from .config import TentsConfig
4
+ from .game import TentsGame
5
+
6
+ __all__ = ["TentsGame", "TentsConfig"]
@@ -0,0 +1,24 @@
1
+ """Configuration for Tents game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class TentsConfig(BaseModel):
9
+ """Configuration for Tents and Trees game."""
10
+
11
+ difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
12
+ size: int = Field(ge=6, le=10, description="Grid size (NxN)")
13
+ num_trees: int = Field(ge=4, description="Number of tree-tent pairs")
14
+
15
+ @classmethod
16
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "TentsConfig":
17
+ """Create config from difficulty level."""
18
+ config_map = {
19
+ DifficultyLevel.EASY: {"size": 6, "num_trees": 6},
20
+ DifficultyLevel.MEDIUM: {"size": 8, "num_trees": 10},
21
+ DifficultyLevel.HARD: {"size": 10, "num_trees": 15},
22
+ }
23
+ params = config_map[difficulty]
24
+ return cls(difficulty=difficulty, **params)