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,399 @@
1
+ """Kakuro 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 KakuroConfig
8
+
9
+
10
+ class KakuroGame(PuzzleGame):
11
+ """Kakuro (Cross Sums) puzzle game.
12
+
13
+ Like a crossword puzzle but with numbers. Each run (horizontal or vertical
14
+ sequence of white cells) must sum to the clue number, and no digit can repeat
15
+ within a run.
16
+ """
17
+
18
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
19
+ """Initialize a new Kakuro game.
20
+
21
+ Args:
22
+ difficulty: Game difficulty level (easy=4x4, medium=6x6, hard=8x8)
23
+ """
24
+ super().__init__(difficulty, seed, **kwargs)
25
+
26
+ # Use pydantic config based on difficulty
27
+ self.config = KakuroConfig.from_difficulty(self.difficulty)
28
+ self.size = self.config.size
29
+
30
+ # Grid: 0 = empty/playable, -1 = black cell
31
+ self.grid = [[0 for _ in range(self.size)] for _ in range(self.size)]
32
+ self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
33
+ self.initial_grid = [[0 for _ in range(self.size)] for _ in range(self.size)]
34
+
35
+ # Clues: list of (row, col, direction, sum, length)
36
+ # direction: 'h' for horizontal, 'v' for vertical
37
+ self.clues: list[tuple[int, int, str, int, int]] = []
38
+
39
+ @property
40
+ def name(self) -> str:
41
+ """The display name of this puzzle type."""
42
+ return "Kakuro"
43
+
44
+ @property
45
+ def description(self) -> str:
46
+ """A one-line description of this puzzle type."""
47
+ return "Crossword math puzzle - fill runs with unique digits that sum to clues"
48
+
49
+ @property
50
+ def constraint_types(self) -> list[str]:
51
+ """Constraint types demonstrated by this puzzle."""
52
+ return ["linear_sum", "all_different_in_run", "clue_satisfaction", "regional_constraints"]
53
+
54
+ @property
55
+ def business_analogies(self) -> list[str]:
56
+ """Business problems this puzzle models."""
57
+ return ["budget_allocation", "sum_constraints", "unique_distribution", "financial_planning"]
58
+
59
+ @property
60
+ def complexity_profile(self) -> dict[str, str]:
61
+ """Complexity profile of this puzzle."""
62
+ return {"reasoning_type": "deductive", "search_space": "medium", "constraint_density": "moderate"}
63
+
64
+ @property
65
+ def optimal_steps(self) -> int | None:
66
+ """Minimum steps = white cells to fill."""
67
+ if not hasattr(self, "grid") or not self.grid:
68
+ return None
69
+ return sum(1 for r in range(len(self.grid)) for c in range(len(self.grid[0])) if self.grid[r][c] == 0)
70
+
71
+ @property
72
+ def difficulty_profile(self) -> "DifficultyProfile":
73
+ """Difficulty characteristics for Kakuro."""
74
+ from ...models import DifficultyLevel
75
+
76
+ logic_depth = {
77
+ DifficultyLevel.EASY.value: 2,
78
+ DifficultyLevel.MEDIUM.value: 4,
79
+ DifficultyLevel.HARD.value: 5,
80
+ }.get(self.difficulty.value, 3)
81
+ return DifficultyProfile(
82
+ logic_depth=logic_depth,
83
+ branching_factor=3.0,
84
+ state_observability=1.0,
85
+ constraint_density=0.6,
86
+ )
87
+
88
+ def _is_cell_in_run(self, row: int, col: int) -> bool:
89
+ """Check if a cell is part of at least one run of length >= 2."""
90
+ if self.grid[row][col] == -1:
91
+ return True # Black cells don't need to be in runs
92
+
93
+ # Check horizontal run
94
+ h_count = 1
95
+ # Count left
96
+ c = col - 1
97
+ while c >= 0 and self.grid[row][c] != -1:
98
+ h_count += 1
99
+ c -= 1
100
+ # Count right
101
+ c = col + 1
102
+ while c < self.size and self.grid[row][c] != -1:
103
+ h_count += 1
104
+ c += 1
105
+
106
+ if h_count >= 2:
107
+ return True
108
+
109
+ # Check vertical run
110
+ v_count = 1
111
+ # Count up
112
+ r = row - 1
113
+ while r >= 0 and self.grid[r][col] != -1:
114
+ v_count += 1
115
+ r -= 1
116
+ # Count down
117
+ r = row + 1
118
+ while r < self.size and self.grid[r][col] != -1:
119
+ v_count += 1
120
+ r += 1
121
+
122
+ return v_count >= 2
123
+
124
+ def _create_pattern(self) -> None:
125
+ """Create a pattern of black and white cells ensuring all white cells are in runs."""
126
+ max_attempts = 50
127
+
128
+ for _attempt in range(max_attempts):
129
+ # Simple pattern: create some black cells
130
+ self.grid = [[0 for _ in range(self.size)] for _ in range(self.size)]
131
+
132
+ # Make top-left corner black (standard Kakuro pattern)
133
+ self.grid[0][0] = -1
134
+
135
+ # Add first row and first column as black (clue cells)
136
+ # This creates a more standard Kakuro layout
137
+ if self.size >= 4:
138
+ # Make first column mostly black for clues
139
+ for r in range(self.size):
140
+ if self._rng.random() < 0.5:
141
+ self.grid[r][0] = -1
142
+
143
+ # Add some random black cells to create runs
144
+ num_black = self.size // 2
145
+ for _ in range(num_black):
146
+ row = self._rng.randint(1, self.size - 1)
147
+ col = self._rng.randint(1, self.size - 1)
148
+ if self.grid[row][col] == 0:
149
+ self.grid[row][col] = -1
150
+
151
+ # Verify all white cells are in runs of length >= 2
152
+ all_valid = True
153
+ for r in range(self.size):
154
+ for c in range(self.size):
155
+ if self.grid[r][c] == 0 and not self._is_cell_in_run(r, c):
156
+ # This cell is orphaned - make it black
157
+ self.grid[r][c] = -1
158
+
159
+ # Re-check that we still have enough white cells
160
+ white_count = sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == 0)
161
+ if white_count >= self.size * 2: # Need at least some playable cells
162
+ # Final verification
163
+ all_valid = True
164
+ for r in range(self.size):
165
+ for c in range(self.size):
166
+ if self.grid[r][c] == 0 and not self._is_cell_in_run(r, c):
167
+ all_valid = False
168
+ break
169
+ if not all_valid:
170
+ break
171
+
172
+ if all_valid:
173
+ return
174
+
175
+ # Fallback: create a simple valid pattern
176
+ self.grid = [[0 for _ in range(self.size)] for _ in range(self.size)]
177
+ self.grid[0][0] = -1
178
+ # Make a simple cross pattern
179
+ mid = self.size // 2
180
+ self.grid[mid][mid] = -1
181
+
182
+ def _find_runs(self) -> list[tuple[int, int, str, list[tuple[int, int]]]]:
183
+ """Find all runs (sequences of white cells) in the grid.
184
+
185
+ Returns:
186
+ List of (start_row, start_col, direction, cells) where cells is list of (row, col)
187
+ """
188
+ runs = []
189
+
190
+ # Find horizontal runs
191
+ for row in range(self.size):
192
+ col = 0
193
+ while col < self.size:
194
+ if self.grid[row][col] != -1:
195
+ # Start of a run
196
+ start_col = col
197
+ cells = []
198
+ while col < self.size and self.grid[row][col] != -1:
199
+ cells.append((row, col))
200
+ col += 1
201
+
202
+ if len(cells) >= 2: # Only count runs of length 2+
203
+ runs.append((row, start_col, "h", cells))
204
+ else:
205
+ col += 1
206
+
207
+ # Find vertical runs
208
+ for col in range(self.size):
209
+ row = 0
210
+ while row < self.size:
211
+ if self.grid[row][col] != -1:
212
+ # Start of a run
213
+ start_row = row
214
+ cells = []
215
+ while row < self.size and self.grid[row][col] != -1:
216
+ cells.append((row, col))
217
+ row += 1
218
+
219
+ if len(cells) >= 2: # Only count runs of length 2+
220
+ runs.append((start_row, col, "v", cells))
221
+ else:
222
+ row += 1
223
+
224
+ return runs
225
+
226
+ async def generate_puzzle(self) -> None:
227
+ """Generate a new Kakuro puzzle."""
228
+ # Create pattern
229
+ self._create_pattern()
230
+
231
+ # Find all runs
232
+ runs = self._find_runs()
233
+
234
+ # Generate solution for each run
235
+ self.solution = [row[:] for row in self.grid]
236
+ self.clues = []
237
+
238
+ for start_row, start_col, direction, cells in runs:
239
+ # Generate unique random digits for this run
240
+ run_length = len(cells)
241
+ digits = self._rng.sample(range(1, 10), run_length)
242
+
243
+ for (r, c), digit in zip(cells, digits, strict=True):
244
+ self.solution[r][c] = digit
245
+
246
+ # Create clue
247
+ clue_sum = sum(digits)
248
+ self.clues.append((start_row, start_col, direction, clue_sum, run_length))
249
+
250
+ # Empty the playable cells
251
+ for row in range(self.size):
252
+ for col in range(self.size):
253
+ if self.grid[row][col] != -1:
254
+ self.grid[row][col] = 0
255
+
256
+ self.initial_grid = [row[:] for row in self.grid]
257
+ self.moves_made = 0
258
+ self.game_started = True
259
+
260
+ async def validate_move(self, row: int, col: int, num: int) -> MoveResult:
261
+ """Place a number on the grid.
262
+
263
+ Args:
264
+ row: Row index (1-indexed, user-facing)
265
+ col: Column index (1-indexed, user-facing)
266
+ num: Number to place (1-9, or 0 to clear)
267
+
268
+ Returns:
269
+ MoveResult with success status and message
270
+ """
271
+ # Convert to 0-indexed
272
+ row -= 1
273
+ col -= 1
274
+
275
+ # Validate coordinates
276
+ if not (0 <= row < self.size and 0 <= col < self.size):
277
+ return MoveResult(success=False, message=f"Invalid coordinates. Use row and column between 1-{self.size}.")
278
+
279
+ # Check if this is a black cell
280
+ if self.initial_grid[row][col] == -1:
281
+ return MoveResult(success=False, message="Cannot place numbers in black cells.")
282
+
283
+ # Clear the cell
284
+ if num == 0:
285
+ self.grid[row][col] = 0
286
+ return MoveResult(success=True, message="Cell cleared.", state_changed=True)
287
+
288
+ # Validate number
289
+ if not (1 <= num <= 9):
290
+ return MoveResult(success=False, message="Invalid number. Use 1-9 or 0 to clear.")
291
+
292
+ self.grid[row][col] = num
293
+ self.moves_made += 1
294
+ return MoveResult(success=True, message="Number placed successfully!", state_changed=True)
295
+
296
+ def is_complete(self) -> bool:
297
+ """Check if the puzzle is complete and correct."""
298
+ # Check all white cells filled
299
+ for row in range(self.size):
300
+ for col in range(self.size):
301
+ if self.grid[row][col] == 0: # Empty white cell
302
+ return False
303
+ if self.grid[row][col] != -1 and self.grid[row][col] != self.solution[row][col]:
304
+ return False
305
+
306
+ return True
307
+
308
+ async def get_hint(self) -> tuple[Any, str] | None:
309
+ """Get a hint for the next move.
310
+
311
+ Returns:
312
+ Tuple of (hint_data, hint_message) or None if puzzle is complete
313
+ """
314
+ empty_cells = [
315
+ (r, c)
316
+ for r in range(self.size)
317
+ for c in range(self.size)
318
+ if self.grid[r][c] == 0 and self.solution[r][c] > 0 # Empty white cell with valid solution
319
+ ]
320
+ if not empty_cells:
321
+ return None
322
+
323
+ row, col = self._rng.choice(empty_cells)
324
+ hint_data = (row + 1, col + 1, self.solution[row][col])
325
+ hint_message = f"Try placing {self.solution[row][col]} at row {row + 1}, column {col + 1}"
326
+ return hint_data, hint_message
327
+
328
+ def render_grid(self) -> str:
329
+ """Render the current puzzle state as ASCII art.
330
+
331
+ Returns:
332
+ String representation of the puzzle grid
333
+ """
334
+ lines = []
335
+
336
+ # Header - align with row format "N |"
337
+ header = " |"
338
+ for i in range(self.size):
339
+ header += f" {i + 1} |"
340
+ lines.append(header)
341
+ lines.append(" +" + "---+" * self.size)
342
+
343
+ for row in range(self.size):
344
+ line = f"{row + 1} |"
345
+ for col in range(self.size):
346
+ if self.grid[row][col] == -1:
347
+ line += " ■ |"
348
+ else:
349
+ cell = self.grid[row][col]
350
+ cell_str = str(cell) if cell != 0 else "."
351
+ line += f" {cell_str} |"
352
+ lines.append(line)
353
+ lines.append(" +" + "---+" * self.size)
354
+
355
+ # Show clues
356
+ lines.append("\nClues:")
357
+ for start_row, start_col, direction, clue_sum, length in self.clues:
358
+ dir_str = "→" if direction == "h" else "↓"
359
+ lines.append(f" ({start_row + 1},{start_col + 1}) {dir_str} {clue_sum} ({length} cells)")
360
+
361
+ return "\n".join(lines)
362
+
363
+ def get_rules(self) -> str:
364
+ """Get the rules description for Kakuro.
365
+
366
+ Returns:
367
+ Multi-line string describing the puzzle rules
368
+ """
369
+ return """KAKURO RULES:
370
+ - Fill white cells with 1-9
371
+ - Runs must sum to clue (→ horizontal, ↓ vertical)
372
+ - No repeats within a run
373
+ - Black cells (■) stay empty"""
374
+
375
+ def get_commands(self) -> str:
376
+ """Get the available commands for Kakuro.
377
+
378
+ Returns:
379
+ Multi-line string describing available commands
380
+ """
381
+ return """KAKURO COMMANDS:
382
+ place <row> <col> <num> - Place a number (e.g., 'place 1 2 4')
383
+ clear <row> <col> - Clear a cell
384
+ show - Display the current grid
385
+ hint - Get a hint for the next move
386
+ check - Check your progress
387
+ solve - Show the solution (ends game)
388
+ menu - Return to game selection
389
+ quit - Exit the server"""
390
+
391
+ def get_stats(self) -> str:
392
+ """Get current game statistics.
393
+
394
+ Returns:
395
+ String with game stats
396
+ """
397
+ empty = sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == 0)
398
+ total_white = sum(1 for r in range(self.size) for c in range(self.size) if self.initial_grid[r][c] != -1)
399
+ return f"Moves made: {self.moves_made} | Empty cells: {empty}/{total_white} | Seed: {self.seed}"
@@ -0,0 +1,6 @@
1
+ """KenKen puzzle game module."""
2
+
3
+ from .config import KenKenConfig
4
+ from .game import KenKenGame
5
+
6
+ __all__ = ["KenKenGame", "KenKenConfig"]
@@ -0,0 +1,24 @@
1
+ """Configuration for KenKen game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class KenKenConfig(BaseModel):
9
+ """Configuration for KenKen game."""
10
+
11
+ difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
12
+ size: int = Field(ge=3, le=9, description="Grid size (NxN)")
13
+ num_cages: int = Field(ge=1, description="Number of cages")
14
+
15
+ @classmethod
16
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "KenKenConfig":
17
+ """Create config from difficulty level."""
18
+ config_map = {
19
+ DifficultyLevel.EASY: {"size": 4, "num_cages": 8},
20
+ DifficultyLevel.MEDIUM: {"size": 5, "num_cages": 12},
21
+ DifficultyLevel.HARD: {"size": 6, "num_cages": 18},
22
+ }
23
+ params = config_map[difficulty]
24
+ return cls(difficulty=difficulty, **params)
@@ -0,0 +1,13 @@
1
+ """KenKen game enums."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class ArithmeticOperation(str, Enum):
7
+ """Arithmetic operations for KenKen cages."""
8
+
9
+ ADD = "+"
10
+ SUBTRACT = "-"
11
+ MULTIPLY = "*"
12
+ DIVIDE = "/"
13
+ NONE = "" # For single-cell cages