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,337 @@
|
|
|
1
|
+
"""Abstract base class for all puzzle games."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...models import DifficultyLevel, DifficultyProfile, MoveResult, SolverConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PuzzleGame(ABC):
|
|
11
|
+
"""Base class for all puzzle games in the arcade.
|
|
12
|
+
|
|
13
|
+
This defines the common interface that all puzzle types must implement.
|
|
14
|
+
Games are pure puzzle generators - they don't solve, they just validate.
|
|
15
|
+
The solving happens client-side (LLMs with MCP solver access).
|
|
16
|
+
|
|
17
|
+
All games support deterministic seeding for reproducibility:
|
|
18
|
+
- Pass a seed to __init__ to get the same puzzle every time
|
|
19
|
+
- Use self._rng for all random operations in subclasses
|
|
20
|
+
- The seed is exposed so players can share puzzles
|
|
21
|
+
|
|
22
|
+
Metrics tracked for evaluation:
|
|
23
|
+
- moves_made: Valid moves (state-changing actions)
|
|
24
|
+
- invalid_moves: Rejected/invalid move attempts
|
|
25
|
+
- hints_used: Solver hints consumed
|
|
26
|
+
- retries: Attempts on same cell (backtracking indicator)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
difficulty: DifficultyLevel | str = DifficultyLevel.EASY,
|
|
32
|
+
seed: int | None = None,
|
|
33
|
+
solver_config: SolverConfig | None = None,
|
|
34
|
+
):
|
|
35
|
+
"""Initialize a new puzzle game.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
difficulty: Game difficulty level (easy, medium, hard)
|
|
39
|
+
seed: Random seed for reproducible puzzle generation.
|
|
40
|
+
If None, a random seed is generated.
|
|
41
|
+
solver_config: Configuration for solver/hint usage.
|
|
42
|
+
If None, uses default (solver allowed, no penalty).
|
|
43
|
+
"""
|
|
44
|
+
# Convert string to enum if needed (for backwards compatibility)
|
|
45
|
+
if isinstance(difficulty, str):
|
|
46
|
+
self.difficulty = DifficultyLevel(difficulty)
|
|
47
|
+
else:
|
|
48
|
+
self.difficulty = difficulty
|
|
49
|
+
|
|
50
|
+
# Initialize deterministic random number generator
|
|
51
|
+
self.seed = seed if seed is not None else random.randint(0, 2**32 - 1)
|
|
52
|
+
self._rng = random.Random(self.seed)
|
|
53
|
+
|
|
54
|
+
# Solver configuration
|
|
55
|
+
self.solver_config = solver_config or SolverConfig()
|
|
56
|
+
|
|
57
|
+
# Core metrics
|
|
58
|
+
self.moves_made = 0
|
|
59
|
+
self.invalid_moves = 0
|
|
60
|
+
self.hints_used = 0
|
|
61
|
+
self.retries = 0 # Attempts on same cell
|
|
62
|
+
|
|
63
|
+
# State tracking
|
|
64
|
+
self.game_started = False
|
|
65
|
+
self._last_move_position: tuple[Any, ...] | None = None # For retry detection
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
async def generate_puzzle(self) -> None:
|
|
69
|
+
"""Generate a new puzzle with a unique solution.
|
|
70
|
+
|
|
71
|
+
This should create the puzzle grid, store the solution,
|
|
72
|
+
and prepare the initial state for play.
|
|
73
|
+
|
|
74
|
+
This is async to allow for non-blocking generation of complex puzzles.
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
async def validate_move(self, *args: Any, **kwargs: Any) -> MoveResult:
|
|
80
|
+
"""Validate a player's move.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
*args: Move parameters (game-specific)
|
|
84
|
+
**kwargs: Additional move parameters (game-specific)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
MoveResult containing success status and message
|
|
88
|
+
"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def is_complete(self) -> bool:
|
|
93
|
+
"""Check if the puzzle is completely and correctly solved.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if puzzle is solved correctly, False otherwise
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
async def get_hint(self) -> tuple[Any, str] | None:
|
|
102
|
+
"""Get a hint for the next move.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple of (hint_data, hint_message) or None if no hints available
|
|
106
|
+
|
|
107
|
+
This is async to allow for complex hint computation.
|
|
108
|
+
"""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def render_grid(self) -> str:
|
|
113
|
+
"""Render the current puzzle state as ASCII art.
|
|
114
|
+
|
|
115
|
+
This should be clean and parseable for LLM clients.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
String representation of the puzzle grid
|
|
119
|
+
"""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def get_rules(self) -> str:
|
|
124
|
+
"""Get the rules description for this puzzle type.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Multi-line string describing the puzzle rules
|
|
128
|
+
"""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def get_commands(self) -> str:
|
|
133
|
+
"""Get the available commands for this puzzle type.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Multi-line string describing available commands
|
|
137
|
+
"""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def get_stats(self) -> str:
|
|
141
|
+
"""Get current game statistics.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
String with game stats (moves, completion, etc.)
|
|
145
|
+
"""
|
|
146
|
+
parts = [f"Moves: {self.moves_made}"]
|
|
147
|
+
if self.invalid_moves > 0:
|
|
148
|
+
parts.append(f"Invalid: {self.invalid_moves}")
|
|
149
|
+
if self.hints_used > 0:
|
|
150
|
+
parts.append(f"Hints: {self.hints_used}")
|
|
151
|
+
parts.append(f"Seed: {self.seed}")
|
|
152
|
+
return " | ".join(parts)
|
|
153
|
+
|
|
154
|
+
def record_move(self, position: tuple[Any, ...], success: bool) -> None:
|
|
155
|
+
"""Record a move attempt for metrics tracking.
|
|
156
|
+
|
|
157
|
+
Call this from validate_move() implementations to track metrics.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
position: The position/target of the move (for retry detection)
|
|
161
|
+
success: Whether the move was valid
|
|
162
|
+
"""
|
|
163
|
+
if success:
|
|
164
|
+
self.moves_made += 1
|
|
165
|
+
else:
|
|
166
|
+
self.invalid_moves += 1
|
|
167
|
+
|
|
168
|
+
# Detect retries (same position attempted again)
|
|
169
|
+
if self._last_move_position == position:
|
|
170
|
+
self.retries += 1
|
|
171
|
+
self._last_move_position = position
|
|
172
|
+
|
|
173
|
+
def record_hint(self) -> bool:
|
|
174
|
+
"""Record a hint request and check if allowed.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
True if hint is allowed, False if budget exceeded or solver disabled.
|
|
178
|
+
"""
|
|
179
|
+
if not self.solver_config.solver_allowed:
|
|
180
|
+
return False
|
|
181
|
+
if self.hints_used >= self.solver_config.hint_budget:
|
|
182
|
+
return False
|
|
183
|
+
self.hints_used += 1
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
def can_use_hint(self) -> bool:
|
|
187
|
+
"""Check if hints are available without consuming one.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if solver is allowed and budget not exceeded.
|
|
191
|
+
"""
|
|
192
|
+
if not self.solver_config.solver_allowed:
|
|
193
|
+
return False
|
|
194
|
+
return self.hints_used < self.solver_config.hint_budget
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def hints_remaining(self) -> int:
|
|
198
|
+
"""Number of hints remaining in budget."""
|
|
199
|
+
if not self.solver_config.solver_allowed:
|
|
200
|
+
return 0
|
|
201
|
+
return max(0, self.solver_config.hint_budget - self.hints_used)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
@abstractmethod
|
|
205
|
+
def name(self) -> str:
|
|
206
|
+
"""The display name of this puzzle type."""
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def description(self) -> str:
|
|
212
|
+
"""A one-line description of this puzzle type."""
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def constraint_types(self) -> list[str]:
|
|
217
|
+
"""The types of constraints this puzzle demonstrates.
|
|
218
|
+
|
|
219
|
+
Examples: all_different, linear_sum, boolean_sat, optimization,
|
|
220
|
+
connectivity, global_loop, feedback, probabilistic
|
|
221
|
+
"""
|
|
222
|
+
return []
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def business_analogies(self) -> list[str]:
|
|
226
|
+
"""Real-world business problems this puzzle models.
|
|
227
|
+
|
|
228
|
+
Examples: scheduling, resource_allocation, portfolio_selection,
|
|
229
|
+
routing, capacity_planning, constraint_satisfaction
|
|
230
|
+
"""
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def complexity_profile(self) -> dict[str, str]:
|
|
235
|
+
"""Complexity characteristics of this puzzle.
|
|
236
|
+
|
|
237
|
+
Returns dict with:
|
|
238
|
+
- reasoning_type: deductive, probabilistic, optimization, hybrid
|
|
239
|
+
- search_space: small, medium, large, exponential
|
|
240
|
+
- constraint_density: sparse, moderate, dense
|
|
241
|
+
"""
|
|
242
|
+
return {"reasoning_type": "deductive", "search_space": "medium", "constraint_density": "moderate"}
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def complexity_metrics(self) -> dict[str, int | float]:
|
|
246
|
+
"""Quantified complexity metrics for this puzzle instance.
|
|
247
|
+
|
|
248
|
+
Returns dict with:
|
|
249
|
+
- variable_count: Number of decision variables (cells to fill)
|
|
250
|
+
- constraint_count: Number of constraints
|
|
251
|
+
- domain_size: Average domain size per variable
|
|
252
|
+
- branching_factor: Estimated branching factor
|
|
253
|
+
- empty_cells: Number of cells still to be filled
|
|
254
|
+
|
|
255
|
+
Override in subclasses for accurate values.
|
|
256
|
+
"""
|
|
257
|
+
return {
|
|
258
|
+
"variable_count": 0,
|
|
259
|
+
"constraint_count": 0,
|
|
260
|
+
"domain_size": 0,
|
|
261
|
+
"branching_factor": 0.0,
|
|
262
|
+
"empty_cells": 0,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def difficulty_profile(self) -> DifficultyProfile:
|
|
267
|
+
"""Detailed difficulty characteristics for curriculum learning.
|
|
268
|
+
|
|
269
|
+
Goes beyond simple easy/medium/hard to enable:
|
|
270
|
+
- Curriculum learning with skill ladders
|
|
271
|
+
- Fair comparisons across identical difficulty profiles
|
|
272
|
+
- Automated training runs with reproducible difficulty scaling
|
|
273
|
+
|
|
274
|
+
Override in subclasses for accurate values based on puzzle instance.
|
|
275
|
+
"""
|
|
276
|
+
# Default values based on difficulty level
|
|
277
|
+
base_logic = {DifficultyLevel.EASY.value: 2, DifficultyLevel.MEDIUM.value: 4, DifficultyLevel.HARD.value: 6}
|
|
278
|
+
base_branching = {
|
|
279
|
+
DifficultyLevel.EASY.value: 2.0,
|
|
280
|
+
DifficultyLevel.MEDIUM.value: 4.0,
|
|
281
|
+
DifficultyLevel.HARD.value: 6.0,
|
|
282
|
+
}
|
|
283
|
+
base_density = {
|
|
284
|
+
DifficultyLevel.EASY.value: 0.6,
|
|
285
|
+
DifficultyLevel.MEDIUM.value: 0.5,
|
|
286
|
+
DifficultyLevel.HARD.value: 0.4,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
diff_str = self.difficulty.value
|
|
290
|
+
return DifficultyProfile(
|
|
291
|
+
logic_depth=base_logic.get(diff_str, 3),
|
|
292
|
+
branching_factor=base_branching.get(diff_str, 3.0),
|
|
293
|
+
state_observability=1.0, # Most puzzles are fully observable
|
|
294
|
+
constraint_density=base_density.get(diff_str, 0.5),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def optimal_steps(self) -> int | None:
|
|
299
|
+
"""Minimum number of steps to solve this puzzle (from solver).
|
|
300
|
+
|
|
301
|
+
Returns None if not computed or not applicable.
|
|
302
|
+
Override in subclasses that can compute optimal solutions.
|
|
303
|
+
|
|
304
|
+
For grid-based puzzles, this is typically the number of empty cells.
|
|
305
|
+
For optimization puzzles, this may be the number of decisions.
|
|
306
|
+
"""
|
|
307
|
+
# Default: estimate from complexity metrics
|
|
308
|
+
metrics = self.complexity_metrics
|
|
309
|
+
empty_cells = metrics.get("empty_cells", 0)
|
|
310
|
+
if empty_cells > 0:
|
|
311
|
+
return int(empty_cells)
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def canonical_solution(self) -> list[tuple[Any, ...]] | None:
|
|
316
|
+
"""Optimal solution trace as a list of moves.
|
|
317
|
+
|
|
318
|
+
Returns None if not available.
|
|
319
|
+
Override in subclasses that can provide solution traces.
|
|
320
|
+
|
|
321
|
+
Each move is a tuple of (row, col, value) or game-specific format.
|
|
322
|
+
"""
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
def get_solution_efficiency(self, steps_taken: int) -> float:
|
|
326
|
+
"""Calculate efficiency score compared to optimal solution.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
steps_taken: Actual steps taken to solve
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Efficiency score from 0.0 to 1.0 (1.0 = optimal)
|
|
333
|
+
"""
|
|
334
|
+
optimal = self.optimal_steps
|
|
335
|
+
if optimal is None or steps_taken == 0:
|
|
336
|
+
return 0.0
|
|
337
|
+
return min(1.0, optimal / steps_taken)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Configuration for Binary Puzzle game."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ...models.enums import DifficultyLevel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BinaryConfig(BaseModel):
|
|
9
|
+
"""Configuration for Binary Puzzle game."""
|
|
10
|
+
|
|
11
|
+
difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
|
|
12
|
+
size: int = Field(ge=4, le=14, description="Grid size (NxN, must be even)")
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_difficulty(cls, difficulty: DifficultyLevel) -> "BinaryConfig":
|
|
16
|
+
"""Create config from difficulty level."""
|
|
17
|
+
config_map = {
|
|
18
|
+
DifficultyLevel.EASY: {"size": 6},
|
|
19
|
+
DifficultyLevel.MEDIUM: {"size": 8},
|
|
20
|
+
DifficultyLevel.HARD: {"size": 10},
|
|
21
|
+
}
|
|
22
|
+
params = config_map[difficulty]
|
|
23
|
+
return cls(difficulty=difficulty, **params)
|