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,434 @@
|
|
|
1
|
+
"""Binary 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 BinaryConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BinaryPuzzleGame(PuzzleGame):
|
|
11
|
+
"""Binary Puzzle (also known as Takuzu or Binairo).
|
|
12
|
+
|
|
13
|
+
Fill a grid with 0s and 1s following these rules:
|
|
14
|
+
- No more than two consecutive 0s or 1s in any row or column
|
|
15
|
+
- Each row and column must have equal numbers of 0s and 1s
|
|
16
|
+
- No two rows are identical, no two columns are identical
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
|
|
20
|
+
"""Initialize a new Binary Puzzle game.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
difficulty: Game difficulty level (easy=6x6, medium=8x8, hard=10x10)
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(difficulty, seed, **kwargs)
|
|
26
|
+
|
|
27
|
+
# Grid size based on difficulty (must be even)
|
|
28
|
+
self.config = BinaryConfig.from_difficulty(self.difficulty)
|
|
29
|
+
self.size = self.config.size
|
|
30
|
+
|
|
31
|
+
# Grid: -1 = empty, 0 or 1 = filled
|
|
32
|
+
self.grid = [[-1 for _ in range(self.size)] for _ in range(self.size)]
|
|
33
|
+
self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
|
34
|
+
self.initial_grid = [[-1 for _ in range(self.size)] for _ in range(self.size)]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def name(self) -> str:
|
|
38
|
+
"""The display name of this puzzle type."""
|
|
39
|
+
return "Binary Puzzle"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def description(self) -> str:
|
|
43
|
+
"""A one-line description of this puzzle type."""
|
|
44
|
+
return "Fill grid with 0s and 1s - no three in a row, equal counts"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def constraint_types(self) -> list[str]:
|
|
48
|
+
"""Constraint types demonstrated by this puzzle."""
|
|
49
|
+
return ["all_different", "no_three_consecutive", "equal_counts", "pattern_avoidance"]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def business_analogies(self) -> list[str]:
|
|
53
|
+
"""Business problems this puzzle models."""
|
|
54
|
+
return ["binary_allocation", "balanced_distribution", "pattern_constraints", "quota_management"]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def complexity_profile(self) -> dict[str, str]:
|
|
58
|
+
"""Complexity profile of this puzzle."""
|
|
59
|
+
return {"reasoning_type": "deductive", "search_space": "medium", "constraint_density": "moderate"}
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def optimal_steps(self) -> int | None:
|
|
63
|
+
"""Minimum steps = empty cells to fill."""
|
|
64
|
+
return sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def difficulty_profile(self) -> "DifficultyProfile":
|
|
68
|
+
"""Difficulty characteristics for Binary Puzzle."""
|
|
69
|
+
|
|
70
|
+
empty = self.optimal_steps or 0
|
|
71
|
+
total = self.size * self.size
|
|
72
|
+
logic_depth = {
|
|
73
|
+
DifficultyLevel.EASY.value: 2,
|
|
74
|
+
DifficultyLevel.MEDIUM.value: 3,
|
|
75
|
+
DifficultyLevel.HARD.value: 4,
|
|
76
|
+
}.get(self.difficulty.value, 2)
|
|
77
|
+
branching = 2.0 # Binary choice per cell
|
|
78
|
+
density = 1.0 - (empty / total) if total > 0 else 0.5
|
|
79
|
+
return DifficultyProfile(
|
|
80
|
+
logic_depth=logic_depth,
|
|
81
|
+
branching_factor=branching,
|
|
82
|
+
state_observability=1.0,
|
|
83
|
+
constraint_density=round(density, 2),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _check_no_three_consecutive(self, grid: list[list[int]]) -> bool:
|
|
87
|
+
"""Check if there are no three consecutive 0s or 1s.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
grid: The grid to check
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if valid, False if three consecutive found
|
|
94
|
+
"""
|
|
95
|
+
# Check rows
|
|
96
|
+
for row in range(self.size):
|
|
97
|
+
for col in range(self.size - 2):
|
|
98
|
+
vals = [grid[row][col], grid[row][col + 1], grid[row][col + 2]]
|
|
99
|
+
if -1 not in vals and len(set(vals)) == 1:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Check columns
|
|
103
|
+
for col in range(self.size):
|
|
104
|
+
for row in range(self.size - 2):
|
|
105
|
+
vals = [grid[row][col], grid[row + 1][col], grid[row + 2][col]]
|
|
106
|
+
if -1 not in vals and len(set(vals)) == 1:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def _check_equal_counts(self, sequence: list[int]) -> bool:
|
|
112
|
+
"""Check if a completed sequence has equal 0s and 1s.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
sequence: List of values
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if equal counts or incomplete, False if counts are wrong
|
|
119
|
+
"""
|
|
120
|
+
if -1 in sequence:
|
|
121
|
+
# Not complete yet
|
|
122
|
+
count_0 = sequence.count(0)
|
|
123
|
+
count_1 = sequence.count(1)
|
|
124
|
+
# Check if we haven't exceeded the limit
|
|
125
|
+
return count_0 <= self.size // 2 and count_1 <= self.size // 2
|
|
126
|
+
|
|
127
|
+
return sequence.count(0) == sequence.count(1) == self.size // 2
|
|
128
|
+
|
|
129
|
+
def _generate_valid_solution(self) -> bool:
|
|
130
|
+
"""Generate a valid binary puzzle solution using backtracking.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if solution generated successfully
|
|
134
|
+
"""
|
|
135
|
+
for row in range(self.size):
|
|
136
|
+
for col in range(self.size):
|
|
137
|
+
if self.solution[row][col] == -1:
|
|
138
|
+
# Try 0 and 1
|
|
139
|
+
for value in [0, 1]:
|
|
140
|
+
self.solution[row][col] = value
|
|
141
|
+
|
|
142
|
+
# Check constraints
|
|
143
|
+
if self._check_no_three_consecutive(self.solution):
|
|
144
|
+
# Check row count constraint
|
|
145
|
+
row_vals = self.solution[row]
|
|
146
|
+
if self._check_equal_counts(row_vals):
|
|
147
|
+
# Check column count constraint
|
|
148
|
+
col_vals = [self.solution[r][col] for r in range(self.size)]
|
|
149
|
+
if self._check_equal_counts(col_vals):
|
|
150
|
+
if self._generate_valid_solution():
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
self.solution[row][col] = -1
|
|
154
|
+
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
def _verify_binary_solution(self) -> bool:
|
|
160
|
+
"""Verify the solution satisfies all binary puzzle constraints."""
|
|
161
|
+
# Check all rows
|
|
162
|
+
for row in range(self.size):
|
|
163
|
+
row_vals = self.solution[row]
|
|
164
|
+
if row_vals.count(0) != self.size // 2 or row_vals.count(1) != self.size // 2:
|
|
165
|
+
return False
|
|
166
|
+
# Check no three consecutive
|
|
167
|
+
for col in range(self.size - 2):
|
|
168
|
+
if row_vals[col] == row_vals[col + 1] == row_vals[col + 2]:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
# Check all columns
|
|
172
|
+
for col in range(self.size):
|
|
173
|
+
col_vals = [self.solution[r][col] for r in range(self.size)]
|
|
174
|
+
if col_vals.count(0) != self.size // 2 or col_vals.count(1) != self.size // 2:
|
|
175
|
+
return False
|
|
176
|
+
# Check no three consecutive
|
|
177
|
+
for row in range(self.size - 2):
|
|
178
|
+
if col_vals[row] == col_vals[row + 1] == col_vals[row + 2]:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
def _create_fallback_binary_solution(self) -> None:
|
|
184
|
+
"""Create a simple valid binary solution using alternating pattern."""
|
|
185
|
+
# Use a known valid pattern: alternating 0011 style
|
|
186
|
+
self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
|
187
|
+
|
|
188
|
+
for row in range(self.size):
|
|
189
|
+
for col in range(self.size):
|
|
190
|
+
# Create a pattern that avoids three consecutive
|
|
191
|
+
# Pattern: 0,0,1,1,0,0 for even rows, 1,1,0,0,1,1 for odd rows
|
|
192
|
+
block = (col // 2) % 2
|
|
193
|
+
if row % 2 == 1:
|
|
194
|
+
block = 1 - block
|
|
195
|
+
self.solution[row][col] = block
|
|
196
|
+
|
|
197
|
+
def _generate_valid_binary_row(self, row: int) -> list[int] | None:
|
|
198
|
+
"""Generate a valid row that satisfies all constraints.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
row: Row index to generate
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Valid pattern or None if generation fails
|
|
205
|
+
"""
|
|
206
|
+
# Create pattern with equal 0s and 1s
|
|
207
|
+
pattern = [0] * (self.size // 2) + [1] * (self.size // 2)
|
|
208
|
+
|
|
209
|
+
for _ in range(200):
|
|
210
|
+
self._rng.shuffle(pattern)
|
|
211
|
+
|
|
212
|
+
# Check this row doesn't have three consecutive
|
|
213
|
+
has_three = False
|
|
214
|
+
for col in range(self.size - 2):
|
|
215
|
+
if pattern[col] == pattern[col + 1] == pattern[col + 2]:
|
|
216
|
+
has_three = True
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
if has_three:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Check column constraints so far
|
|
223
|
+
valid = True
|
|
224
|
+
for col in range(self.size):
|
|
225
|
+
col_vals = [self.solution[r][col] for r in range(row)] + [pattern[col]]
|
|
226
|
+
if col_vals.count(0) > self.size // 2 or col_vals.count(1) > self.size // 2:
|
|
227
|
+
valid = False
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
# Check no three consecutive in column
|
|
231
|
+
if row >= 2:
|
|
232
|
+
if pattern[col] == self.solution[row - 1][col] == self.solution[row - 2][col]:
|
|
233
|
+
valid = False
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
if valid:
|
|
237
|
+
return pattern[:]
|
|
238
|
+
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
async def generate_puzzle(self) -> None:
|
|
242
|
+
"""Generate a new Binary Puzzle."""
|
|
243
|
+
max_restarts = 20
|
|
244
|
+
|
|
245
|
+
for _ in range(max_restarts):
|
|
246
|
+
# Start with empty solution
|
|
247
|
+
self.solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
|
248
|
+
|
|
249
|
+
# Try to generate each row
|
|
250
|
+
success = True
|
|
251
|
+
for row in range(self.size):
|
|
252
|
+
pattern = self._generate_valid_binary_row(row)
|
|
253
|
+
if pattern is None:
|
|
254
|
+
success = False
|
|
255
|
+
break
|
|
256
|
+
self.solution[row] = pattern
|
|
257
|
+
|
|
258
|
+
if success:
|
|
259
|
+
# Verify the solution
|
|
260
|
+
if self._verify_binary_solution():
|
|
261
|
+
break
|
|
262
|
+
else:
|
|
263
|
+
# Fallback: use a simple alternating pattern
|
|
264
|
+
self._create_fallback_binary_solution()
|
|
265
|
+
|
|
266
|
+
# Remove some cells based on difficulty
|
|
267
|
+
cells_to_remove_map = {
|
|
268
|
+
DifficultyLevel.EASY: self.size * 2,
|
|
269
|
+
DifficultyLevel.MEDIUM: self.size * 3,
|
|
270
|
+
DifficultyLevel.HARD: self.size * 4,
|
|
271
|
+
}
|
|
272
|
+
cells_to_remove = cells_to_remove_map[self.difficulty]
|
|
273
|
+
|
|
274
|
+
# Copy solution to grid
|
|
275
|
+
self.grid = [row[:] for row in self.solution]
|
|
276
|
+
|
|
277
|
+
# Randomly remove cells
|
|
278
|
+
cells = [(r, c) for r in range(self.size) for c in range(self.size)]
|
|
279
|
+
self._rng.shuffle(cells)
|
|
280
|
+
|
|
281
|
+
for r, c in cells[:cells_to_remove]:
|
|
282
|
+
self.grid[r][c] = -1
|
|
283
|
+
|
|
284
|
+
self.initial_grid = [row[:] for row in self.grid]
|
|
285
|
+
self.moves_made = 0
|
|
286
|
+
self.game_started = True
|
|
287
|
+
|
|
288
|
+
async def validate_move(self, row: int, col: int, num: int) -> MoveResult:
|
|
289
|
+
"""Place a number on the grid.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
row: Row index (1-indexed, user-facing)
|
|
293
|
+
col: Column index (1-indexed, user-facing)
|
|
294
|
+
num: Number to place (0, 1, or -1 to clear)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
MoveResult indicating success/failure and message
|
|
298
|
+
"""
|
|
299
|
+
# Convert to 0-indexed
|
|
300
|
+
row -= 1
|
|
301
|
+
col -= 1
|
|
302
|
+
|
|
303
|
+
# Validate coordinates
|
|
304
|
+
if not (0 <= row < self.size and 0 <= col < self.size):
|
|
305
|
+
return MoveResult(success=False, message=f"Invalid coordinates. Use row and column between 1-{self.size}.")
|
|
306
|
+
|
|
307
|
+
# Check if this cell is part of the initial puzzle
|
|
308
|
+
if self.initial_grid[row][col] != -1:
|
|
309
|
+
return MoveResult(success=False, message="Cannot modify initial puzzle cells.")
|
|
310
|
+
|
|
311
|
+
# Clear the cell
|
|
312
|
+
if num == -1 or num == 2: # Accept 2 as clear command for convenience
|
|
313
|
+
self.grid[row][col] = -1
|
|
314
|
+
return MoveResult(success=True, message="Cell cleared.", state_changed=True)
|
|
315
|
+
|
|
316
|
+
# Validate number
|
|
317
|
+
if num not in [0, 1]:
|
|
318
|
+
return MoveResult(success=False, message="Invalid number. Use 0, 1, or 2 to clear.")
|
|
319
|
+
|
|
320
|
+
# Check if the move is valid
|
|
321
|
+
old_value = self.grid[row][col]
|
|
322
|
+
self.grid[row][col] = num
|
|
323
|
+
|
|
324
|
+
# Check no three consecutive
|
|
325
|
+
if not self._check_no_three_consecutive(self.grid):
|
|
326
|
+
self.grid[row][col] = old_value
|
|
327
|
+
return MoveResult(success=False, message="Invalid move! This creates three consecutive identical values.")
|
|
328
|
+
|
|
329
|
+
# Check count constraints
|
|
330
|
+
row_vals = self.grid[row]
|
|
331
|
+
if not self._check_equal_counts(row_vals):
|
|
332
|
+
self.grid[row][col] = old_value
|
|
333
|
+
return MoveResult(success=False, message="Invalid move! This exceeds the count limit for this row.")
|
|
334
|
+
|
|
335
|
+
col_vals = [self.grid[r][col] for r in range(self.size)]
|
|
336
|
+
if not self._check_equal_counts(col_vals):
|
|
337
|
+
self.grid[row][col] = old_value
|
|
338
|
+
return MoveResult(success=False, message="Invalid move! This exceeds the count limit for this column.")
|
|
339
|
+
|
|
340
|
+
self.moves_made += 1
|
|
341
|
+
return MoveResult(success=True, message="Number placed successfully!", state_changed=True)
|
|
342
|
+
|
|
343
|
+
def is_complete(self) -> bool:
|
|
344
|
+
"""Check if the puzzle is complete and correct."""
|
|
345
|
+
# Check all cells filled
|
|
346
|
+
for row in range(self.size):
|
|
347
|
+
for col in range(self.size):
|
|
348
|
+
if self.grid[row][col] == -1:
|
|
349
|
+
return False
|
|
350
|
+
if self.grid[row][col] != self.solution[row][col]:
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
async def get_hint(self) -> tuple[Any, str] | None:
|
|
356
|
+
"""Get a hint for the next move.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Tuple of (hint_data, hint_message) or None if puzzle is complete
|
|
360
|
+
"""
|
|
361
|
+
empty_cells = [(r, c) for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1]
|
|
362
|
+
if not empty_cells:
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
row, col = self._rng.choice(empty_cells)
|
|
366
|
+
hint_data = (row + 1, col + 1, self.solution[row][col])
|
|
367
|
+
hint_message = f"Try placing {self.solution[row][col]} at row {row + 1}, column {col + 1}"
|
|
368
|
+
return hint_data, hint_message
|
|
369
|
+
|
|
370
|
+
def render_grid(self) -> str:
|
|
371
|
+
"""Render the current puzzle state as ASCII art.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
String representation of the puzzle grid
|
|
375
|
+
"""
|
|
376
|
+
lines = []
|
|
377
|
+
|
|
378
|
+
# Header - align with row format "NN|"
|
|
379
|
+
header = " |"
|
|
380
|
+
for i in range(self.size):
|
|
381
|
+
col_label = str(i + 1) if i < 9 else chr(65 + i - 9)
|
|
382
|
+
header += f"{col_label}|"
|
|
383
|
+
lines.append(header)
|
|
384
|
+
lines.append(" +" + "-+" * self.size)
|
|
385
|
+
|
|
386
|
+
for row in range(self.size):
|
|
387
|
+
line = f"{row + 1:2d}|"
|
|
388
|
+
for col in range(self.size):
|
|
389
|
+
cell = self.grid[row][col]
|
|
390
|
+
if cell == -1:
|
|
391
|
+
line += ".|"
|
|
392
|
+
else:
|
|
393
|
+
line += f"{cell}|"
|
|
394
|
+
lines.append(line)
|
|
395
|
+
lines.append(" +" + "-+" * self.size)
|
|
396
|
+
|
|
397
|
+
return "\n".join(lines)
|
|
398
|
+
|
|
399
|
+
def get_rules(self) -> str:
|
|
400
|
+
"""Get the rules description for Binary Puzzle.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Multi-line string describing the puzzle rules
|
|
404
|
+
"""
|
|
405
|
+
return f"""BINARY PUZZLE RULES:
|
|
406
|
+
- Fill {self.size}x{self.size} grid with 0s and 1s
|
|
407
|
+
- Max 2 consecutive 0s or 1s per row/column
|
|
408
|
+
- Each row/column: {self.size // 2} zeros, {self.size // 2} ones
|
|
409
|
+
- All rows unique, all columns unique"""
|
|
410
|
+
|
|
411
|
+
def get_commands(self) -> str:
|
|
412
|
+
"""Get the available commands for Binary Puzzle.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Multi-line string describing available commands
|
|
416
|
+
"""
|
|
417
|
+
return """BINARY PUZZLE COMMANDS:
|
|
418
|
+
place <row> <col> <num> - Place 0 or 1 (e.g., 'place 1 2 0')
|
|
419
|
+
clear <row> <col> - Clear a cell (or use 'place <row> <col> 2')
|
|
420
|
+
show - Display the current grid
|
|
421
|
+
hint - Get a hint for the next move
|
|
422
|
+
check - Check your progress
|
|
423
|
+
solve - Show the solution (ends game)
|
|
424
|
+
menu - Return to game selection
|
|
425
|
+
quit - Exit the server"""
|
|
426
|
+
|
|
427
|
+
def get_stats(self) -> str:
|
|
428
|
+
"""Get current game statistics.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
String with game stats
|
|
432
|
+
"""
|
|
433
|
+
empty = sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == -1)
|
|
434
|
+
return f"Moves made: {self.moves_made} | Empty cells: {empty} | Grid size: {self.size}x{self.size} | Seed: {self.seed}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Configuration for Bridges game."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ...models.enums import DifficultyLevel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BridgesConfig(BaseModel):
|
|
9
|
+
"""Configuration for Bridges game."""
|
|
10
|
+
|
|
11
|
+
difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
|
|
12
|
+
size: int = Field(ge=5, le=10, description="Grid size (NxN)")
|
|
13
|
+
num_islands: int = Field(ge=4, description="Number of islands")
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_difficulty(cls, difficulty: DifficultyLevel) -> "BridgesConfig":
|
|
17
|
+
"""Create config from difficulty level."""
|
|
18
|
+
config_map = {
|
|
19
|
+
DifficultyLevel.EASY: {"size": 5, "num_islands": 5},
|
|
20
|
+
DifficultyLevel.MEDIUM: {"size": 7, "num_islands": 8},
|
|
21
|
+
DifficultyLevel.HARD: {"size": 9, "num_islands": 12},
|
|
22
|
+
}
|
|
23
|
+
params = config_map[difficulty]
|
|
24
|
+
return cls(difficulty=difficulty, **params)
|