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.
- chuk_puzzles_gym/__init__.py +19 -0
- chuk_puzzles_gym/constants.py +9 -0
- chuk_puzzles_gym/eval.py +763 -0
- chuk_puzzles_gym/export/__init__.py +20 -0
- chuk_puzzles_gym/export/dataset.py +376 -0
- chuk_puzzles_gym/games/__init__.py +94 -0
- chuk_puzzles_gym/games/_base/__init__.py +6 -0
- chuk_puzzles_gym/games/_base/commands.py +91 -0
- chuk_puzzles_gym/games/_base/game.py +337 -0
- chuk_puzzles_gym/games/binary/__init__.py +6 -0
- chuk_puzzles_gym/games/binary/config.py +23 -0
- chuk_puzzles_gym/games/binary/game.py +434 -0
- chuk_puzzles_gym/games/bridges/__init__.py +6 -0
- chuk_puzzles_gym/games/bridges/config.py +24 -0
- chuk_puzzles_gym/games/bridges/game.py +489 -0
- chuk_puzzles_gym/games/einstein/__init__.py +6 -0
- chuk_puzzles_gym/games/einstein/config.py +23 -0
- chuk_puzzles_gym/games/einstein/constants.py +13 -0
- chuk_puzzles_gym/games/einstein/game.py +366 -0
- chuk_puzzles_gym/games/einstein/models.py +35 -0
- chuk_puzzles_gym/games/fillomino/__init__.py +6 -0
- chuk_puzzles_gym/games/fillomino/config.py +24 -0
- chuk_puzzles_gym/games/fillomino/game.py +516 -0
- chuk_puzzles_gym/games/futoshiki/__init__.py +6 -0
- chuk_puzzles_gym/games/futoshiki/config.py +23 -0
- chuk_puzzles_gym/games/futoshiki/game.py +391 -0
- chuk_puzzles_gym/games/hidato/__init__.py +6 -0
- chuk_puzzles_gym/games/hidato/config.py +24 -0
- chuk_puzzles_gym/games/hidato/game.py +403 -0
- chuk_puzzles_gym/games/hitori/__init__.py +6 -0
- chuk_puzzles_gym/games/hitori/config.py +23 -0
- chuk_puzzles_gym/games/hitori/game.py +451 -0
- chuk_puzzles_gym/games/kakuro/__init__.py +6 -0
- chuk_puzzles_gym/games/kakuro/config.py +24 -0
- chuk_puzzles_gym/games/kakuro/game.py +399 -0
- chuk_puzzles_gym/games/kenken/__init__.py +6 -0
- chuk_puzzles_gym/games/kenken/config.py +24 -0
- chuk_puzzles_gym/games/kenken/enums.py +13 -0
- chuk_puzzles_gym/games/kenken/game.py +486 -0
- chuk_puzzles_gym/games/kenken/models.py +15 -0
- chuk_puzzles_gym/games/killer_sudoku/__init__.py +6 -0
- chuk_puzzles_gym/games/killer_sudoku/config.py +23 -0
- chuk_puzzles_gym/games/killer_sudoku/game.py +502 -0
- chuk_puzzles_gym/games/killer_sudoku/models.py +15 -0
- chuk_puzzles_gym/games/knapsack/__init__.py +6 -0
- chuk_puzzles_gym/games/knapsack/config.py +24 -0
- chuk_puzzles_gym/games/knapsack/enums.py +10 -0
- chuk_puzzles_gym/games/knapsack/game.py +340 -0
- chuk_puzzles_gym/games/knapsack/models.py +13 -0
- chuk_puzzles_gym/games/lights_out/__init__.py +6 -0
- chuk_puzzles_gym/games/lights_out/config.py +24 -0
- chuk_puzzles_gym/games/lights_out/game.py +249 -0
- chuk_puzzles_gym/games/logic_grid/__init__.py +6 -0
- chuk_puzzles_gym/games/logic_grid/config.py +24 -0
- chuk_puzzles_gym/games/logic_grid/constants.py +12 -0
- chuk_puzzles_gym/games/logic_grid/game.py +333 -0
- chuk_puzzles_gym/games/logic_grid/models.py +24 -0
- chuk_puzzles_gym/games/mastermind/__init__.py +6 -0
- chuk_puzzles_gym/games/mastermind/config.py +25 -0
- chuk_puzzles_gym/games/mastermind/game.py +297 -0
- chuk_puzzles_gym/games/minesweeper/__init__.py +6 -0
- chuk_puzzles_gym/games/minesweeper/config.py +24 -0
- chuk_puzzles_gym/games/minesweeper/enums.py +12 -0
- chuk_puzzles_gym/games/minesweeper/game.py +432 -0
- chuk_puzzles_gym/games/nonogram/__init__.py +6 -0
- chuk_puzzles_gym/games/nonogram/config.py +23 -0
- chuk_puzzles_gym/games/nonogram/game.py +296 -0
- chuk_puzzles_gym/games/nurikabe/__init__.py +6 -0
- chuk_puzzles_gym/games/nurikabe/config.py +24 -0
- chuk_puzzles_gym/games/nurikabe/enums.py +14 -0
- chuk_puzzles_gym/games/nurikabe/game.py +586 -0
- chuk_puzzles_gym/games/scheduler/__init__.py +6 -0
- chuk_puzzles_gym/games/scheduler/config.py +25 -0
- chuk_puzzles_gym/games/scheduler/constants.py +15 -0
- chuk_puzzles_gym/games/scheduler/enums.py +10 -0
- chuk_puzzles_gym/games/scheduler/game.py +431 -0
- chuk_puzzles_gym/games/scheduler/models.py +14 -0
- chuk_puzzles_gym/games/shikaku/__init__.py +6 -0
- chuk_puzzles_gym/games/shikaku/config.py +24 -0
- chuk_puzzles_gym/games/shikaku/game.py +419 -0
- chuk_puzzles_gym/games/slitherlink/__init__.py +6 -0
- chuk_puzzles_gym/games/slitherlink/config.py +23 -0
- chuk_puzzles_gym/games/slitherlink/game.py +386 -0
- chuk_puzzles_gym/games/sokoban/__init__.py +6 -0
- chuk_puzzles_gym/games/sokoban/config.py +24 -0
- chuk_puzzles_gym/games/sokoban/game.py +671 -0
- chuk_puzzles_gym/games/star_battle/__init__.py +6 -0
- chuk_puzzles_gym/games/star_battle/config.py +24 -0
- chuk_puzzles_gym/games/star_battle/game.py +390 -0
- chuk_puzzles_gym/games/sudoku/__init__.py +7 -0
- chuk_puzzles_gym/games/sudoku/commands.py +96 -0
- chuk_puzzles_gym/games/sudoku/config.py +22 -0
- chuk_puzzles_gym/games/sudoku/game.py +328 -0
- chuk_puzzles_gym/games/tents/__init__.py +6 -0
- chuk_puzzles_gym/games/tents/config.py +24 -0
- chuk_puzzles_gym/games/tents/game.py +416 -0
- chuk_puzzles_gym/gym_env.py +465 -0
- chuk_puzzles_gym/models/__init__.py +47 -0
- chuk_puzzles_gym/models/base.py +30 -0
- chuk_puzzles_gym/models/config.py +11 -0
- chuk_puzzles_gym/models/enums.py +104 -0
- chuk_puzzles_gym/models/evaluation.py +487 -0
- chuk_puzzles_gym/models/games.py +12 -0
- chuk_puzzles_gym/server.py +1171 -0
- chuk_puzzles_gym/trace/__init__.py +10 -0
- chuk_puzzles_gym/trace/generator.py +726 -0
- chuk_puzzles_gym/utils/__init__.py +4 -0
- chuk_puzzles_gym-0.9.dist-info/METADATA +1471 -0
- chuk_puzzles_gym-0.9.dist-info/RECORD +112 -0
- chuk_puzzles_gym-0.9.dist-info/WHEEL +5 -0
- chuk_puzzles_gym-0.9.dist-info/entry_points.txt +4 -0
- chuk_puzzles_gym-0.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Nonogram (Picross) 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 NonogramConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NonogramGame(PuzzleGame):
|
|
11
|
+
"""Nonogram (also known as Picross, Griddlers, or Hanjie) puzzle game.
|
|
12
|
+
|
|
13
|
+
Fill cells to reveal a picture based on number clues for each row and column.
|
|
14
|
+
Clues indicate consecutive filled cells in that row/column.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
|
|
18
|
+
"""Initialize a new Nonogram game.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
difficulty: Game difficulty level (easy=5x5, medium=7x7, hard=10x10)
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(difficulty, seed, **kwargs)
|
|
24
|
+
|
|
25
|
+
# Use pydantic config based on difficulty
|
|
26
|
+
self.config = NonogramConfig.from_difficulty(self.difficulty)
|
|
27
|
+
self.size = self.config.size
|
|
28
|
+
|
|
29
|
+
# Grid: -1 = unknown, 0 = empty (marked X), 1 = filled (marked ■)
|
|
30
|
+
self.grid = [[-1 for _ in range(self.size)] for _ in range(self.size)]
|
|
31
|
+
self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
|
32
|
+
self.initial_grid = [[-1 for _ in range(self.size)] for _ in range(self.size)]
|
|
33
|
+
|
|
34
|
+
# Clues: row_clues[i] = list of consecutive filled counts for row i
|
|
35
|
+
self.row_clues: list[list[int]] = []
|
|
36
|
+
self.col_clues: list[list[int]] = []
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def name(self) -> str:
|
|
40
|
+
"""The display name of this puzzle type."""
|
|
41
|
+
return "Nonogram"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def description(self) -> str:
|
|
45
|
+
"""A one-line description of this puzzle type."""
|
|
46
|
+
return "Picture logic puzzle - reveal image from number clues"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def constraint_types(self) -> list[str]:
|
|
50
|
+
"""Constraint types demonstrated by this puzzle."""
|
|
51
|
+
return ["run_length_encoding", "linear_constraints", "cross_referencing", "pattern_completion"]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def business_analogies(self) -> list[str]:
|
|
55
|
+
"""Business problems this puzzle models."""
|
|
56
|
+
return ["data_reconstruction", "pattern_recognition", "image_recovery", "constraint_propagation"]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def complexity_profile(self) -> dict[str, str]:
|
|
60
|
+
"""Complexity profile of this puzzle."""
|
|
61
|
+
return {"reasoning_type": "deductive", "search_space": "large", "constraint_density": "dense"}
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def optimal_steps(self) -> int | None:
|
|
65
|
+
"""Minimum steps = all cells to mark (filled=1 and empty=0)."""
|
|
66
|
+
if not hasattr(self, "solution") or not self.solution:
|
|
67
|
+
return None
|
|
68
|
+
# Count both filled (1) and empty (0) cells - all need to be marked
|
|
69
|
+
return sum(
|
|
70
|
+
1 for r in range(len(self.solution)) for c in range(len(self.solution[0])) if self.solution[r][c] in (0, 1)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def difficulty_profile(self) -> "DifficultyProfile":
|
|
75
|
+
"""Difficulty characteristics for Nonogram."""
|
|
76
|
+
|
|
77
|
+
logic_depth = {
|
|
78
|
+
DifficultyLevel.EASY.value: 2,
|
|
79
|
+
DifficultyLevel.MEDIUM.value: 4,
|
|
80
|
+
DifficultyLevel.HARD.value: 5,
|
|
81
|
+
}.get(self.difficulty.value, 3)
|
|
82
|
+
return DifficultyProfile(
|
|
83
|
+
logic_depth=logic_depth,
|
|
84
|
+
branching_factor=2.0,
|
|
85
|
+
state_observability=1.0,
|
|
86
|
+
constraint_density=0.5,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _calculate_clues(self, line: list[int]) -> list[int]:
|
|
90
|
+
"""Calculate clues for a line (row or column).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
line: List of 0s and 1s
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of consecutive filled cell counts
|
|
97
|
+
"""
|
|
98
|
+
clues = []
|
|
99
|
+
count = 0
|
|
100
|
+
|
|
101
|
+
for cell in line:
|
|
102
|
+
if cell == 1:
|
|
103
|
+
count += 1
|
|
104
|
+
elif count > 0:
|
|
105
|
+
clues.append(count)
|
|
106
|
+
count = 0
|
|
107
|
+
|
|
108
|
+
if count > 0:
|
|
109
|
+
clues.append(count)
|
|
110
|
+
|
|
111
|
+
return clues if clues else [0]
|
|
112
|
+
|
|
113
|
+
def _generate_pattern(self) -> None:
|
|
114
|
+
"""Generate a random pattern for the solution."""
|
|
115
|
+
# Create a simple random pattern
|
|
116
|
+
density_map = {
|
|
117
|
+
DifficultyLevel.EASY: 0.4,
|
|
118
|
+
DifficultyLevel.MEDIUM: 0.5,
|
|
119
|
+
DifficultyLevel.HARD: 0.6,
|
|
120
|
+
}
|
|
121
|
+
density = density_map[self.difficulty]
|
|
122
|
+
|
|
123
|
+
for row in range(self.size):
|
|
124
|
+
for col in range(self.size):
|
|
125
|
+
self.solution[row][col] = 1 if self._rng.random() < density else 0
|
|
126
|
+
|
|
127
|
+
async def generate_puzzle(self) -> None:
|
|
128
|
+
"""Generate a new Nonogram puzzle."""
|
|
129
|
+
# Generate a random pattern
|
|
130
|
+
self._generate_pattern()
|
|
131
|
+
|
|
132
|
+
# Calculate clues from the solution
|
|
133
|
+
self.row_clues = []
|
|
134
|
+
for row in range(self.size):
|
|
135
|
+
clues = self._calculate_clues(self.solution[row])
|
|
136
|
+
self.row_clues.append(clues)
|
|
137
|
+
|
|
138
|
+
self.col_clues = []
|
|
139
|
+
for col in range(self.size):
|
|
140
|
+
column = [self.solution[row][col] for row in range(self.size)]
|
|
141
|
+
clues = self._calculate_clues(column)
|
|
142
|
+
self.col_clues.append(clues)
|
|
143
|
+
|
|
144
|
+
# Start with empty grid
|
|
145
|
+
self.grid = [[-1 for _ in range(self.size)] for _ in range(self.size)]
|
|
146
|
+
self.initial_grid = [row[:] for row in self.grid]
|
|
147
|
+
self.moves_made = 0
|
|
148
|
+
self.game_started = True
|
|
149
|
+
|
|
150
|
+
async def validate_move(self, row: int, col: int, value: int) -> MoveResult:
|
|
151
|
+
"""Mark a cell on the grid.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
row: Row index (1-indexed, user-facing)
|
|
155
|
+
col: Column index (1-indexed, user-facing)
|
|
156
|
+
value: Value to place (0=empty/X, 1=filled/■, -1=unknown/clear)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
MoveResult with success status and message
|
|
160
|
+
"""
|
|
161
|
+
# Convert to 0-indexed
|
|
162
|
+
row -= 1
|
|
163
|
+
col -= 1
|
|
164
|
+
|
|
165
|
+
# Validate coordinates
|
|
166
|
+
if not (0 <= row < self.size and 0 <= col < self.size):
|
|
167
|
+
return MoveResult(success=False, message=f"Invalid coordinates. Use row and column between 1-{self.size}.")
|
|
168
|
+
|
|
169
|
+
# Validate value
|
|
170
|
+
if value not in [-1, 0, 1]:
|
|
171
|
+
return MoveResult(success=False, message="Invalid value. Use 1 (filled), 0 (empty), or -1 (clear).")
|
|
172
|
+
|
|
173
|
+
self.grid[row][col] = value
|
|
174
|
+
self.moves_made += 1
|
|
175
|
+
return MoveResult(success=True, message="Cell marked successfully!", state_changed=True)
|
|
176
|
+
|
|
177
|
+
def is_complete(self) -> bool:
|
|
178
|
+
"""Check if the puzzle is complete and correct."""
|
|
179
|
+
# Check all cells marked
|
|
180
|
+
for row in range(self.size):
|
|
181
|
+
for col in range(self.size):
|
|
182
|
+
if self.grid[row][col] == -1:
|
|
183
|
+
return False
|
|
184
|
+
if self.grid[row][col] != self.solution[row][col]:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
async def get_hint(self) -> tuple[Any, str] | None:
|
|
190
|
+
"""Get a hint for the next move.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tuple of (hint_data, hint_message) or None if puzzle is complete
|
|
194
|
+
"""
|
|
195
|
+
unknown_cells = [(r, c) for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1]
|
|
196
|
+
if not unknown_cells:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
row, col = self._rng.choice(unknown_cells)
|
|
200
|
+
value = self.solution[row][col]
|
|
201
|
+
value_str = "filled (■)" if value == 1 else "empty (X)"
|
|
202
|
+
hint_data = (row + 1, col + 1, value)
|
|
203
|
+
hint_message = f"Try marking row {row + 1}, column {col + 1} as {value_str}"
|
|
204
|
+
return hint_data, hint_message
|
|
205
|
+
|
|
206
|
+
def render_grid(self) -> str:
|
|
207
|
+
"""Render the current puzzle state as ASCII art.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
String representation of the puzzle grid with clues
|
|
211
|
+
"""
|
|
212
|
+
lines = []
|
|
213
|
+
|
|
214
|
+
# Determine max clue length for formatting
|
|
215
|
+
max_row_clues = max(len(clues) for clues in self.row_clues)
|
|
216
|
+
max_col_clues = max(len(clues) for clues in self.col_clues)
|
|
217
|
+
|
|
218
|
+
# Render column clues
|
|
219
|
+
for clue_idx in range(max_col_clues):
|
|
220
|
+
line = " " * (max_row_clues * 2 + 2)
|
|
221
|
+
for col in range(self.size):
|
|
222
|
+
clues = self.col_clues[col]
|
|
223
|
+
# Pad clues from the top
|
|
224
|
+
padded_idx = clue_idx - (max_col_clues - len(clues))
|
|
225
|
+
if padded_idx >= 0:
|
|
226
|
+
line += f"{clues[padded_idx]:2d} "
|
|
227
|
+
else:
|
|
228
|
+
line += " "
|
|
229
|
+
lines.append(line)
|
|
230
|
+
|
|
231
|
+
lines.append(" " * (max_row_clues * 2 + 2) + "+" + "--+" * self.size)
|
|
232
|
+
|
|
233
|
+
# Render grid with row clues
|
|
234
|
+
for row in range(self.size):
|
|
235
|
+
# Row clues
|
|
236
|
+
clues = self.row_clues[row]
|
|
237
|
+
clue_str = " ".join(f"{c:2d}" for c in clues)
|
|
238
|
+
clue_str = clue_str.rjust(max_row_clues * 3)
|
|
239
|
+
|
|
240
|
+
# Grid row
|
|
241
|
+
line = clue_str + " |"
|
|
242
|
+
for col in range(self.size):
|
|
243
|
+
cell = self.grid[row][col]
|
|
244
|
+
if cell == -1:
|
|
245
|
+
line += " ? |"
|
|
246
|
+
elif cell == 0:
|
|
247
|
+
line += " X |"
|
|
248
|
+
else: # cell == 1
|
|
249
|
+
line += " ■ |"
|
|
250
|
+
lines.append(line)
|
|
251
|
+
lines.append(" " * (max_row_clues * 2 + 2) + "+" + "--+" * self.size)
|
|
252
|
+
|
|
253
|
+
lines.append("\nLegend: ? = unknown, X = empty, ■ = filled")
|
|
254
|
+
|
|
255
|
+
return "\n".join(lines)
|
|
256
|
+
|
|
257
|
+
def get_rules(self) -> str:
|
|
258
|
+
"""Get the rules description for Nonogram.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Multi-line string describing the puzzle rules
|
|
262
|
+
"""
|
|
263
|
+
return f"""NONOGRAM RULES:
|
|
264
|
+
- Fill cells to reveal a picture
|
|
265
|
+
- Numbers on the left show consecutive filled cells in each row
|
|
266
|
+
- Numbers on the top show consecutive filled cells in each column
|
|
267
|
+
- Multiple numbers mean multiple groups with at least one empty cell between
|
|
268
|
+
- For example: [3, 1] means 3 filled, gap, 1 filled
|
|
269
|
+
- Mark cells as: 1 (filled/■), 0 (empty/X), or -1 (unknown/?)
|
|
270
|
+
- Grid size: {self.size}x{self.size}"""
|
|
271
|
+
|
|
272
|
+
def get_commands(self) -> str:
|
|
273
|
+
"""Get the available commands for Nonogram.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Multi-line string describing available commands
|
|
277
|
+
"""
|
|
278
|
+
return """NONOGRAM COMMANDS:
|
|
279
|
+
place <row> <col> <val> - Mark cell: 1=filled(■), 0=empty(X), -1=clear(?)
|
|
280
|
+
Example: 'place 1 2 1' marks (1,2) as filled
|
|
281
|
+
show - Display the current grid
|
|
282
|
+
hint - Get a hint for the next move
|
|
283
|
+
check - Check your progress
|
|
284
|
+
solve - Show the solution (ends game)
|
|
285
|
+
menu - Return to game selection
|
|
286
|
+
quit - Exit the server"""
|
|
287
|
+
|
|
288
|
+
def get_stats(self) -> str:
|
|
289
|
+
"""Get current game statistics.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
String with game stats
|
|
293
|
+
"""
|
|
294
|
+
unknown = sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1)
|
|
295
|
+
filled = sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == 1)
|
|
296
|
+
return f"Moves made: {self.moves_made} | Unknown: {unknown} | Filled: {filled}/{self.size * self.size} | Seed: {self.seed}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Configuration for Nurikabe game."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ...models.enums import DifficultyLevel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NurikabeConfig(BaseModel):
|
|
9
|
+
"""Configuration for Nurikabe game."""
|
|
10
|
+
|
|
11
|
+
difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
|
|
12
|
+
size: int = Field(ge=4, le=12, description="Grid size (NxN)")
|
|
13
|
+
num_islands: int = Field(ge=1, description="Number of islands")
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_difficulty(cls, difficulty: DifficultyLevel) -> "NurikabeConfig":
|
|
17
|
+
"""Create config from difficulty level."""
|
|
18
|
+
config_map = {
|
|
19
|
+
DifficultyLevel.EASY: {"size": 6, "num_islands": 3},
|
|
20
|
+
DifficultyLevel.MEDIUM: {"size": 8, "num_islands": 4},
|
|
21
|
+
DifficultyLevel.HARD: {"size": 10, "num_islands": 5},
|
|
22
|
+
}
|
|
23
|
+
params = config_map[difficulty]
|
|
24
|
+
return cls(difficulty=difficulty, **params)
|