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,366 @@
1
+ """Einstein's Puzzle (Zebra Puzzle) game implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ...models import DifficultyLevel, DifficultyProfile, MoveResult
6
+ from .._base import PuzzleGame
7
+ from .constants import ATTRIBUTES, COLORS, DRINKS, NATIONALITIES, PETS, SMOKES
8
+ from .models import HouseAssignment
9
+
10
+
11
+ class EinsteinGame(PuzzleGame):
12
+ """Einstein's Puzzle (also known as Zebra Puzzle).
13
+
14
+ A classic logic puzzle with 5 houses and 5 attributes each.
15
+ Uses complex deduction with multiple constraint types.
16
+ Perfect for testing AI reasoning capabilities.
17
+ """
18
+
19
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
20
+ """Initialize a new Einstein's Puzzle game.
21
+
22
+ Args:
23
+ difficulty: Game difficulty level (easy/medium/hard)
24
+ """
25
+ super().__init__(difficulty, seed, **kwargs)
26
+
27
+ # 5 houses with 5 attributes each
28
+ self.num_houses = 5
29
+
30
+ # Attributes - using constants for type safety
31
+ self.colors = COLORS
32
+ self.nationalities = NATIONALITIES
33
+ self.drinks = DRINKS
34
+ self.smokes = SMOKES
35
+ self.pets = PETS
36
+
37
+ # Player's assignments: Change from list[dict] to list[HouseAssignment]
38
+ self.assignments: list[HouseAssignment] = [HouseAssignment() for _ in range(self.num_houses)]
39
+
40
+ # Solution
41
+ self.solution: list[HouseAssignment] = []
42
+
43
+ # Clues
44
+ self.clues: list[str] = []
45
+
46
+ @property
47
+ def name(self) -> str:
48
+ """The display name of this puzzle type."""
49
+ return "Einstein's Puzzle"
50
+
51
+ @property
52
+ def description(self) -> str:
53
+ """A one-line description of this puzzle type."""
54
+ return "Classic logic deduction - who owns the fish?"
55
+
56
+ @property
57
+ def constraint_types(self) -> list[str]:
58
+ """Constraint types demonstrated by this puzzle."""
59
+ return ["multi_attribute", "relational", "positional", "logical_implication", "transitive_closure"]
60
+
61
+ @property
62
+ def business_analogies(self) -> list[str]:
63
+ """Business problems this puzzle models."""
64
+ return ["multi_factor_matching", "relationship_mapping", "eligibility_rules", "complex_deduction"]
65
+
66
+ @property
67
+ def complexity_profile(self) -> dict[str, str]:
68
+ """Complexity profile of this puzzle."""
69
+ return {"reasoning_type": "deductive", "search_space": "large", "constraint_density": "dense"}
70
+
71
+ @property
72
+ def optimal_steps(self) -> int | None:
73
+ """Minimum steps = attribute assignments (houses x attributes)."""
74
+ # 5 attributes: color, nationality, drink, smoke, pet
75
+ return self.num_houses * 5
76
+
77
+ @property
78
+ def difficulty_profile(self) -> "DifficultyProfile":
79
+ """Difficulty characteristics for Einstein's Puzzle."""
80
+
81
+ logic_depth = {
82
+ DifficultyLevel.EASY.value: 5,
83
+ DifficultyLevel.MEDIUM.value: 7,
84
+ DifficultyLevel.HARD.value: 9,
85
+ }.get(self.difficulty.value, 6)
86
+ return DifficultyProfile(
87
+ logic_depth=logic_depth,
88
+ branching_factor=5.0, # 5 houses to match
89
+ state_observability=1.0,
90
+ constraint_density=0.7,
91
+ )
92
+
93
+ async def generate_puzzle(self) -> None:
94
+ """Generate a new Einstein's Puzzle."""
95
+ # Generate a random valid solution by shuffling attribute lists
96
+ shuffled_colors = self.colors.copy()
97
+ shuffled_nationalities = self.nationalities.copy()
98
+ shuffled_drinks = self.drinks.copy()
99
+ shuffled_smokes = self.smokes.copy()
100
+ shuffled_pets = self.pets.copy()
101
+
102
+ self._rng.shuffle(shuffled_colors)
103
+ self._rng.shuffle(shuffled_nationalities)
104
+ self._rng.shuffle(shuffled_drinks)
105
+ self._rng.shuffle(shuffled_smokes)
106
+ self._rng.shuffle(shuffled_pets)
107
+
108
+ # Create solution
109
+ self.solution = []
110
+ for i in range(self.num_houses):
111
+ self.solution.append(
112
+ HouseAssignment(
113
+ color=shuffled_colors[i],
114
+ nationality=shuffled_nationalities[i],
115
+ drink=shuffled_drinks[i],
116
+ smoke=shuffled_smokes[i],
117
+ pet=shuffled_pets[i],
118
+ )
119
+ )
120
+
121
+ # Generate clues based on solution
122
+ self.clues = self._generate_clues()
123
+
124
+ # Initialize player grid
125
+ self.assignments = [HouseAssignment() for _ in range(self.num_houses)]
126
+
127
+ self.moves_made = 0
128
+ self.game_started = True
129
+
130
+ def _generate_clues(self) -> list[str]:
131
+ """Generate clues based on the solution."""
132
+ clues = []
133
+
134
+ # Find positions of each attribute
135
+ def find_house(attr_type: str, value: str) -> int:
136
+ for i, house in enumerate(self.solution):
137
+ if house.get_attribute(attr_type) == value:
138
+ return i
139
+ return -1
140
+
141
+ # Always include these starter clues
142
+ norwegian_house = find_house("nationality", "Norwegian")
143
+ milk_house = find_house("drink", "Milk")
144
+
145
+ clues.append(f"1. The Norwegian lives in house {norwegian_house + 1}")
146
+ clues.append(f"2. Milk is drunk in house {milk_house + 1}")
147
+
148
+ # Add attribute-to-attribute clues
149
+ clue_num = 3
150
+
151
+ # Same house clues
152
+ for i in range(self.num_houses):
153
+ house = self.solution[i]
154
+
155
+ # Color-Nationality
156
+ if self._rng.random() < 0.4:
157
+ clues.append(f"{clue_num}. The {house.nationality} lives in the {house.color} house")
158
+ clue_num += 1
159
+
160
+ # Nationality-Drink
161
+ if self._rng.random() < 0.4:
162
+ clues.append(f"{clue_num}. The {house.nationality} drinks {house.drink}")
163
+ clue_num += 1
164
+
165
+ # Smoke-Pet (skip if smoke name has spaces to avoid test parsing issues)
166
+ if self._rng.random() < 0.4 and " " not in house.smoke:
167
+ pet = house.pet
168
+ clues.append(f"{clue_num}. The person who smokes {house.smoke} owns a {pet}")
169
+ clue_num += 1
170
+
171
+ # Neighbor clues
172
+ for i in range(self.num_houses - 1):
173
+ house1 = self.solution[i]
174
+ house2 = self.solution[i + 1]
175
+
176
+ if self._rng.random() < 0.3:
177
+ clues.append(f"{clue_num}. The {house1.color} house is next to the {house2.color} house")
178
+ clue_num += 1
179
+
180
+ # Limit number of clues
181
+ max_clues_map = {
182
+ DifficultyLevel.EASY: 12,
183
+ DifficultyLevel.MEDIUM: 10,
184
+ DifficultyLevel.HARD: 8,
185
+ }
186
+ max_clues = max_clues_map[self.difficulty]
187
+ return clues[:max_clues]
188
+
189
+ async def validate_move(self, house: int, attribute: str, value: str) -> MoveResult:
190
+ """Assign an attribute to a house.
191
+
192
+ Args:
193
+ house: House number (1-indexed, user-facing)
194
+ attribute: Attribute type (color, nationality, drink, smoke, pet)
195
+ value: Attribute value
196
+
197
+ Returns:
198
+ MoveResult indicating success/failure and message
199
+ """
200
+ # Convert to 0-indexed
201
+ house -= 1
202
+
203
+ # Validate house number
204
+ if not (0 <= house < self.num_houses):
205
+ return MoveResult(success=False, message=f"Invalid house number. Use 1-{self.num_houses}.")
206
+
207
+ # Normalize attribute and value
208
+ attribute = attribute.lower()
209
+ value = value.title() # Use title() to handle multi-word values like "Pall Mall"
210
+
211
+ # Handle space-to-hyphen conversion for backward compatibility
212
+ value_normalized = value.replace(" ", "-")
213
+
214
+ # Validate attribute type and get valid values
215
+ valid_attribute_names = ["color", "nationality", "drink", "smoke", "pet"]
216
+ if attribute not in valid_attribute_names:
217
+ return MoveResult(success=False, message=f"Invalid attribute. Use: {', '.join(valid_attribute_names)}")
218
+
219
+ # Get the valid values list for this attribute
220
+ if attribute == "color":
221
+ valid_values = self.colors
222
+ elif attribute == "nationality":
223
+ valid_values = self.nationalities
224
+ elif attribute == "drink":
225
+ valid_values = self.drinks
226
+ elif attribute == "smoke":
227
+ valid_values = self.smokes
228
+ elif attribute == "pet":
229
+ valid_values = self.pets
230
+ else:
231
+ # Should never reach here due to earlier check
232
+ return MoveResult(success=False, message=f"Invalid attribute: {attribute}")
233
+
234
+ # Validate value (try both original and normalized)
235
+ if value in valid_values:
236
+ pass # Value is valid as-is
237
+ elif value_normalized in valid_values:
238
+ value = value_normalized # Use normalized version
239
+ else:
240
+ return MoveResult(success=False, message=f"Invalid {attribute}. Choose from: {', '.join(valid_values)}")
241
+
242
+ # Check if value is already assigned to another house
243
+ for i, other_house in enumerate(self.assignments):
244
+ if i != house and other_house.get_attribute(attribute) == value:
245
+ return MoveResult(success=False, message=f"{value} is already assigned to house {i + 1}")
246
+
247
+ # Check if this house already has a value for this attribute
248
+ if self.assignments[house].get_attribute(attribute) is not None:
249
+ old_value = self.assignments[house].get_attribute(attribute)
250
+ self.assignments[house].set_attribute(attribute, value)
251
+ self.moves_made += 1
252
+ return MoveResult(
253
+ success=True,
254
+ message=f"Changed house {house + 1}'s {attribute} from {old_value} to {value}",
255
+ state_changed=True,
256
+ )
257
+
258
+ # Assign the value
259
+ self.assignments[house].set_attribute(attribute, value)
260
+ self.moves_made += 1
261
+ return MoveResult(success=True, message=f"Assigned {value} to house {house + 1}", state_changed=True)
262
+
263
+ def is_complete(self) -> bool:
264
+ """Check if the puzzle is completely and correctly solved."""
265
+ # All houses must have all attributes assigned
266
+ for house in self.assignments:
267
+ if not house.is_complete():
268
+ return False
269
+
270
+ # Check if assignments match solution
271
+ for i in range(self.num_houses):
272
+ for attr in ATTRIBUTES:
273
+ if self.assignments[i].get_attribute(attr) != self.solution[i].get_attribute(attr):
274
+ return False
275
+
276
+ return True
277
+
278
+ async def get_hint(self) -> tuple[Any, str] | None:
279
+ """Get a hint for the next move.
280
+
281
+ Returns:
282
+ Tuple of (hint_data, hint_message) or None
283
+ """
284
+ # Find first unassigned attribute in solution
285
+ for i in range(self.num_houses):
286
+ for attr in ATTRIBUTES:
287
+ if self.assignments[i].get_attribute(attr) != self.solution[i].get_attribute(attr):
288
+ value = self.solution[i].get_attribute(attr)
289
+ hint_data = (i + 1, attr, value)
290
+ hint_message = f"Try assigning {value} to house {i + 1} as its {attr}"
291
+ return hint_data, hint_message
292
+
293
+ return None
294
+
295
+ def render_grid(self) -> str:
296
+ """Render the current puzzle state as ASCII art.
297
+
298
+ Returns:
299
+ String representation of the puzzle
300
+ """
301
+ lines = []
302
+
303
+ lines.append("Einstein's Puzzle - Who owns the fish?")
304
+ lines.append("")
305
+
306
+ # Houses table
307
+ lines.append("House | Color | Nationality | Drink | Smoke | Pet")
308
+ lines.append("------+---------+-------------+--------+-------------+--------")
309
+
310
+ for i in range(self.num_houses):
311
+ house = self.assignments[i]
312
+ color = house.color or "?"
313
+ nationality = house.nationality or "?"
314
+ drink = house.drink or "?"
315
+ smoke = house.smoke or "?"
316
+ pet = house.pet or "?"
317
+
318
+ lines.append(f" {i + 1} | {color:<7s} | {nationality:<11s} | {drink:<6s} | {smoke:<11s} | {pet:<6s}")
319
+
320
+ lines.append("")
321
+ lines.append("Clues:")
322
+ for clue in self.clues:
323
+ lines.append(f" {clue}")
324
+
325
+ return "\n".join(lines)
326
+
327
+ def get_rules(self) -> str:
328
+ """Get the rules description for Einstein's Puzzle.
329
+
330
+ Returns:
331
+ Multi-line string describing the puzzle rules
332
+ """
333
+ return """EINSTEIN'S PUZZLE RULES:
334
+ - There are 5 houses in a row
335
+ - Each house has a unique color, nationality, drink, smoke, and pet
336
+ - Use the clues to deduce which attribute belongs in which house
337
+ - No attribute can appear in more than one house
338
+ - All houses must have all 5 attributes assigned
339
+ - Question: WHO OWNS THE FISH?"""
340
+
341
+ def get_commands(self) -> str:
342
+ """Get the available commands for Einstein's Puzzle.
343
+
344
+ Returns:
345
+ Multi-line string describing available commands
346
+ """
347
+ return """EINSTEIN'S PUZZLE COMMANDS:
348
+ assign <house> <attr> <value> - Assign attribute (e.g., 'assign 1 color red')
349
+ Attributes: color, nationality, drink, smoke, pet
350
+ show - Display current assignments
351
+ hint - Get a hint
352
+ check - Check if solution is correct
353
+ solve - Show the solution (ends game)
354
+ menu - Return to game selection
355
+ quit - Exit the server"""
356
+
357
+ def get_stats(self) -> str:
358
+ """Get current game statistics.
359
+
360
+ Returns:
361
+ String with game stats
362
+ """
363
+ assigned = sum(1 for house in self.assignments for attr in ATTRIBUTES if house.get_attribute(attr) is not None)
364
+ total = self.num_houses * 5
365
+
366
+ return f"Moves: {self.moves_made} | Assigned: {assigned}/{total} | Clues: {len(self.clues)} | Seed: {self.seed}"
@@ -0,0 +1,35 @@
1
+ """Einstein's Puzzle game models."""
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class HouseAssignment(BaseModel):
7
+ """Attribute assignments for a house in Einstein's Puzzle."""
8
+
9
+ model_config = ConfigDict(frozen=False) # Allow mutation during gameplay
10
+
11
+ color: str | None = None
12
+ nationality: str | None = None
13
+ drink: str | None = None
14
+ smoke: str | None = None
15
+ pet: str | None = None
16
+
17
+ def is_complete(self) -> bool:
18
+ """Check if all attributes are assigned."""
19
+ return all(
20
+ [
21
+ self.color is not None,
22
+ self.nationality is not None,
23
+ self.drink is not None,
24
+ self.smoke is not None,
25
+ self.pet is not None,
26
+ ]
27
+ )
28
+
29
+ def get_attribute(self, attr_type: str) -> str | None:
30
+ """Get attribute value by type."""
31
+ return getattr(self, attr_type.lower(), None)
32
+
33
+ def set_attribute(self, attr_type: str, value: str) -> None:
34
+ """Set attribute value by type."""
35
+ setattr(self, attr_type.lower(), value)
@@ -0,0 +1,6 @@
1
+ """Fillomino puzzle game module."""
2
+
3
+ from .config import FillominoConfig
4
+ from .game import FillominoGame
5
+
6
+ __all__ = ["FillominoGame", "FillominoConfig"]
@@ -0,0 +1,24 @@
1
+ """Configuration for Fillomino game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class FillominoConfig(BaseModel):
9
+ """Configuration for Fillomino 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_clues: int = Field(ge=4, description="Number of clue numbers to reveal")
14
+
15
+ @classmethod
16
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "FillominoConfig":
17
+ """Create config from difficulty level."""
18
+ config_map = {
19
+ DifficultyLevel.EASY: {"size": 6, "num_clues": 8},
20
+ DifficultyLevel.MEDIUM: {"size": 8, "num_clues": 10},
21
+ DifficultyLevel.HARD: {"size": 10, "num_clues": 12},
22
+ }
23
+ params = config_map[difficulty]
24
+ return cls(difficulty=difficulty, **params)