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,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,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)
|