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,489 @@
|
|
|
1
|
+
"""Bridges (Hashiwokakero) puzzle game implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ...models import DifficultyProfile, MoveResult
|
|
6
|
+
from .._base import PuzzleGame
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BridgesGame(PuzzleGame):
|
|
10
|
+
"""Bridges (Hashiwokakero) puzzle game.
|
|
11
|
+
|
|
12
|
+
Connect islands with bridges according to the numbers on each island.
|
|
13
|
+
- Each island must have exactly the number of bridges shown
|
|
14
|
+
- Bridges can only run horizontally or vertically
|
|
15
|
+
- Bridges cannot cross each other
|
|
16
|
+
- At most two bridges can connect any pair of islands
|
|
17
|
+
- All islands must be connected in a single network
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
|
|
21
|
+
"""Initialize a new Bridges game.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
difficulty: Game difficulty level (easy, medium, hard)
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(difficulty, seed, **kwargs)
|
|
27
|
+
|
|
28
|
+
from ...models import DifficultyLevel
|
|
29
|
+
|
|
30
|
+
# Set grid size based on difficulty
|
|
31
|
+
self.size = {
|
|
32
|
+
DifficultyLevel.EASY.value: 7,
|
|
33
|
+
DifficultyLevel.MEDIUM.value: 9,
|
|
34
|
+
DifficultyLevel.HARD.value: 11,
|
|
35
|
+
}.get(self.difficulty.value, 7)
|
|
36
|
+
|
|
37
|
+
# Grid stores island values (0 = water, 1-8 = island with that many bridges needed)
|
|
38
|
+
self.grid: list[list[int]] = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
|
39
|
+
|
|
40
|
+
# Solution stores the bridges (stored as a dict of (r1,c1,r2,c2) -> bridge_count)
|
|
41
|
+
self.solution: dict[tuple[int, int, int, int], int] = {}
|
|
42
|
+
|
|
43
|
+
# Player's current bridges
|
|
44
|
+
self.bridges: dict[tuple[int, int, int, int], int] = {}
|
|
45
|
+
|
|
46
|
+
# Island positions for easy lookup
|
|
47
|
+
self.islands: list[tuple[int, int]] = []
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def name(self) -> str:
|
|
51
|
+
"""The display name of this puzzle type."""
|
|
52
|
+
return "Bridges"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def description(self) -> str:
|
|
56
|
+
"""A one-line description of this puzzle type."""
|
|
57
|
+
return "Connect islands with bridges - satisfy all island numbers"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def constraint_types(self) -> list[str]:
|
|
61
|
+
"""Constraint types demonstrated by this puzzle."""
|
|
62
|
+
return ["connectivity", "local_counting", "graph_construction", "path_finding"]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def business_analogies(self) -> list[str]:
|
|
66
|
+
"""Business problems this puzzle models."""
|
|
67
|
+
return ["network_design", "infrastructure_planning", "connection_optimization", "graph_connectivity"]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def complexity_profile(self) -> dict[str, str]:
|
|
71
|
+
"""Complexity profile of this puzzle."""
|
|
72
|
+
return {"reasoning_type": "deductive", "search_space": "large", "constraint_density": "moderate"}
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def optimal_steps(self) -> int | None:
|
|
76
|
+
"""Minimum steps = number of bridge connections to place."""
|
|
77
|
+
if not hasattr(self, "solution") or not self.solution:
|
|
78
|
+
return None
|
|
79
|
+
# Each connection is one move, regardless of bridge count (1 or 2)
|
|
80
|
+
return len(self.solution)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def difficulty_profile(self) -> "DifficultyProfile":
|
|
84
|
+
"""Difficulty characteristics for Bridges."""
|
|
85
|
+
from ...models import DifficultyLevel
|
|
86
|
+
|
|
87
|
+
logic_depth = {
|
|
88
|
+
DifficultyLevel.EASY.value: 2,
|
|
89
|
+
DifficultyLevel.MEDIUM.value: 4,
|
|
90
|
+
DifficultyLevel.HARD.value: 5,
|
|
91
|
+
}.get(self.difficulty.value, 3)
|
|
92
|
+
return DifficultyProfile(
|
|
93
|
+
logic_depth=logic_depth,
|
|
94
|
+
branching_factor=4.0, # 4 directions, 0-2 bridges
|
|
95
|
+
state_observability=1.0,
|
|
96
|
+
constraint_density=0.5,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _normalize_bridge(self, r1: int, c1: int, r2: int, c2: int) -> tuple[int, int, int, int]:
|
|
100
|
+
"""Normalize bridge coordinates so smaller coords come first."""
|
|
101
|
+
if (r1, c1) > (r2, c2):
|
|
102
|
+
return (r2, c2, r1, c1)
|
|
103
|
+
return (r1, c1, r2, c2)
|
|
104
|
+
|
|
105
|
+
def _find_neighbors(self, row: int, col: int) -> list[tuple[int, int]]:
|
|
106
|
+
"""Find all islands that can be connected from this position."""
|
|
107
|
+
neighbors = []
|
|
108
|
+
|
|
109
|
+
# Check horizontal (left and right)
|
|
110
|
+
for c in range(col + 1, self.size):
|
|
111
|
+
if self.grid[row][c] > 0:
|
|
112
|
+
neighbors.append((row, c))
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
for c in range(col - 1, -1, -1):
|
|
116
|
+
if self.grid[row][c] > 0:
|
|
117
|
+
neighbors.append((row, c))
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
# Check vertical (up and down)
|
|
121
|
+
for r in range(row + 1, self.size):
|
|
122
|
+
if self.grid[r][col] > 0:
|
|
123
|
+
neighbors.append((r, col))
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
for r in range(row - 1, -1, -1):
|
|
127
|
+
if self.grid[r][col] > 0:
|
|
128
|
+
neighbors.append((r, col))
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
return neighbors
|
|
132
|
+
|
|
133
|
+
def _bridges_cross(self, r1: int, c1: int, r2: int, c2: int, r3: int, c3: int, r4: int, c4: int) -> bool:
|
|
134
|
+
"""Check if two bridges would cross each other."""
|
|
135
|
+
# Horizontal bridge 1
|
|
136
|
+
if r1 == r2:
|
|
137
|
+
min_c, max_c = min(c1, c2), max(c1, c2)
|
|
138
|
+
# Vertical bridge 2
|
|
139
|
+
if c3 == c4:
|
|
140
|
+
min_r, max_r = min(r3, r4), max(r3, r4)
|
|
141
|
+
# Check if they intersect
|
|
142
|
+
return min_c < c3 < max_c and min_r < r1 < max_r
|
|
143
|
+
|
|
144
|
+
# Vertical bridge 1
|
|
145
|
+
if c1 == c2:
|
|
146
|
+
min_r, max_r = min(r1, r2), max(r1, r2)
|
|
147
|
+
# Horizontal bridge 2
|
|
148
|
+
if r3 == r4:
|
|
149
|
+
min_c, max_c = min(c3, c4), max(c3, c4)
|
|
150
|
+
# Check if they intersect
|
|
151
|
+
return min_r < r3 < max_r and min_c < c1 < max_c
|
|
152
|
+
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
async def generate_puzzle(self) -> None:
|
|
156
|
+
"""Generate a new Bridges puzzle with retry logic."""
|
|
157
|
+
max_attempts = 50
|
|
158
|
+
|
|
159
|
+
for _attempt in range(max_attempts):
|
|
160
|
+
# Reset state
|
|
161
|
+
self.islands = []
|
|
162
|
+
self.grid = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
|
163
|
+
self.solution = {}
|
|
164
|
+
|
|
165
|
+
# Step 1: Place islands strategically
|
|
166
|
+
if not self._place_islands_strategically():
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# Step 2: Generate solution
|
|
170
|
+
self._generate_solution()
|
|
171
|
+
|
|
172
|
+
# Step 3: Validate solution
|
|
173
|
+
if self._validate_puzzle():
|
|
174
|
+
# Step 4: Update island values based on solution
|
|
175
|
+
for r, c in self.islands:
|
|
176
|
+
bridge_count = 0
|
|
177
|
+
for (r1, c1, r2, c2), count in self.solution.items():
|
|
178
|
+
if (r1, c1) == (r, c) or (r2, c2) == (r, c):
|
|
179
|
+
bridge_count += count
|
|
180
|
+
self.grid[r][c] = bridge_count
|
|
181
|
+
|
|
182
|
+
self.game_started = True
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Fallback: use last attempt even if not perfect
|
|
186
|
+
for r, c in self.islands:
|
|
187
|
+
bridge_count = 0
|
|
188
|
+
for (r1, c1, r2, c2), count in self.solution.items():
|
|
189
|
+
if (r1, c1) == (r, c) or (r2, c2) == (r, c):
|
|
190
|
+
bridge_count += count
|
|
191
|
+
if bridge_count == 0:
|
|
192
|
+
bridge_count = 1
|
|
193
|
+
self.grid[r][c] = bridge_count
|
|
194
|
+
|
|
195
|
+
self.game_started = True
|
|
196
|
+
|
|
197
|
+
def _place_islands_strategically(self) -> bool:
|
|
198
|
+
"""Place islands using strategic positions for better puzzle quality."""
|
|
199
|
+
from ...models import DifficultyLevel
|
|
200
|
+
|
|
201
|
+
num_islands = {
|
|
202
|
+
DifficultyLevel.EASY.value: 8,
|
|
203
|
+
DifficultyLevel.MEDIUM.value: 12,
|
|
204
|
+
DifficultyLevel.HARD.value: 16,
|
|
205
|
+
}.get(self.difficulty.value, 8)
|
|
206
|
+
|
|
207
|
+
# Create a grid of strategic positions (avoid edges for better connectivity)
|
|
208
|
+
step = max(2, self.size // 4)
|
|
209
|
+
strategic_positions = []
|
|
210
|
+
|
|
211
|
+
for r in range(1, self.size - 1, step):
|
|
212
|
+
for c in range(1, self.size - 1, step):
|
|
213
|
+
strategic_positions.append((r, c))
|
|
214
|
+
|
|
215
|
+
# Add some edge positions for variety
|
|
216
|
+
for i in range(1, self.size - 1, step):
|
|
217
|
+
strategic_positions.extend([(0, i), (self.size - 1, i), (i, 0), (i, self.size - 1)])
|
|
218
|
+
|
|
219
|
+
# Shuffle positions for variety
|
|
220
|
+
self._rng.shuffle(strategic_positions)
|
|
221
|
+
|
|
222
|
+
# Select positions ensuring minimum spacing
|
|
223
|
+
min_spacing = 2
|
|
224
|
+
for r, c in strategic_positions:
|
|
225
|
+
if len(self.islands) >= num_islands:
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
# Check spacing from existing islands
|
|
229
|
+
if all(abs(r - ir) + abs(c - ic) >= min_spacing for ir, ic in self.islands):
|
|
230
|
+
self.islands.append((r, c))
|
|
231
|
+
self.grid[r][c] = 1
|
|
232
|
+
|
|
233
|
+
return len(self.islands) >= max(4, num_islands // 2)
|
|
234
|
+
|
|
235
|
+
def _validate_puzzle(self) -> bool:
|
|
236
|
+
"""Validate that the generated puzzle is solvable and well-formed."""
|
|
237
|
+
# Check all islands have at least one bridge
|
|
238
|
+
for r, c in self.islands:
|
|
239
|
+
bridge_count = 0
|
|
240
|
+
for (r1, c1, r2, c2), count in self.solution.items():
|
|
241
|
+
if (r1, c1) == (r, c) or (r2, c2) == (r, c):
|
|
242
|
+
bridge_count += count
|
|
243
|
+
|
|
244
|
+
if bridge_count == 0:
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
# Check bridge count is reasonable (not too high)
|
|
248
|
+
if bridge_count > 8:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
# Check all islands are connected (graph connectivity)
|
|
252
|
+
if not self._check_connectivity():
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
def _check_connectivity(self) -> bool:
|
|
258
|
+
"""Check if all islands are connected via bridges."""
|
|
259
|
+
if not self.islands:
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
# BFS from first island
|
|
263
|
+
visited = {self.islands[0]}
|
|
264
|
+
queue = [self.islands[0]]
|
|
265
|
+
|
|
266
|
+
while queue:
|
|
267
|
+
r, c = queue.pop(0)
|
|
268
|
+
|
|
269
|
+
# Check all bridges from this island
|
|
270
|
+
for (r1, c1, r2, c2), count in self.solution.items():
|
|
271
|
+
if count > 0:
|
|
272
|
+
if (r1, c1) == (r, c) and (r2, c2) not in visited:
|
|
273
|
+
visited.add((r2, c2))
|
|
274
|
+
queue.append((r2, c2))
|
|
275
|
+
elif (r2, c2) == (r, c) and (r1, c1) not in visited:
|
|
276
|
+
visited.add((r1, c1))
|
|
277
|
+
queue.append((r1, c1))
|
|
278
|
+
|
|
279
|
+
return len(visited) == len(self.islands)
|
|
280
|
+
|
|
281
|
+
def _would_cross_existing(self, r1: int, c1: int, r2: int, c2: int) -> bool:
|
|
282
|
+
"""Check if a new bridge would cross any existing bridge in the solution."""
|
|
283
|
+
for (br1, bc1, br2, bc2), bcount in self.solution.items():
|
|
284
|
+
if bcount > 0:
|
|
285
|
+
if self._bridges_cross(r1, c1, r2, c2, br1, bc1, br2, bc2):
|
|
286
|
+
return True
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
def _generate_solution(self) -> None:
|
|
290
|
+
"""Generate a valid solution for the puzzle without crossing bridges."""
|
|
291
|
+
if not self.islands:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
# Create a minimum spanning tree using a simple approach
|
|
295
|
+
# but only add bridges that don't cross existing ones
|
|
296
|
+
connected = {self.islands[0]}
|
|
297
|
+
unconnected = set(self.islands[1:])
|
|
298
|
+
|
|
299
|
+
while unconnected:
|
|
300
|
+
# Find the closest unconnected island to any connected island
|
|
301
|
+
# that can be connected without crossing
|
|
302
|
+
best_dist = float("inf")
|
|
303
|
+
best_pair = None
|
|
304
|
+
|
|
305
|
+
for r1, c1 in connected:
|
|
306
|
+
neighbors = self._find_neighbors(r1, c1)
|
|
307
|
+
for r2, c2 in neighbors:
|
|
308
|
+
if (r2, c2) in unconnected:
|
|
309
|
+
# Check if this bridge would cross existing ones
|
|
310
|
+
if not self._would_cross_existing(r1, c1, r2, c2):
|
|
311
|
+
dist = abs(r1 - r2) + abs(c1 - c2)
|
|
312
|
+
if dist < best_dist:
|
|
313
|
+
best_dist = dist
|
|
314
|
+
best_pair = ((r1, c1), (r2, c2))
|
|
315
|
+
|
|
316
|
+
if best_pair:
|
|
317
|
+
(r1, c1), (r2, c2) = best_pair
|
|
318
|
+
bridge_key = self._normalize_bridge(r1, c1, r2, c2)
|
|
319
|
+
self.solution[bridge_key] = 1
|
|
320
|
+
connected.add((r2, c2))
|
|
321
|
+
unconnected.remove((r2, c2))
|
|
322
|
+
else:
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
# Add some additional bridges for variety (up to 2 bridges per connection)
|
|
326
|
+
# Only if they don't create crossings
|
|
327
|
+
for r1, c1 in self.islands:
|
|
328
|
+
neighbors = self._find_neighbors(r1, c1)
|
|
329
|
+
for r2, c2 in neighbors:
|
|
330
|
+
bridge_key = self._normalize_bridge(r1, c1, r2, c2)
|
|
331
|
+
if bridge_key in self.solution and self._rng.random() < 0.3:
|
|
332
|
+
if self.solution[bridge_key] < 2:
|
|
333
|
+
self.solution[bridge_key] += 1
|
|
334
|
+
|
|
335
|
+
async def validate_move(self, *args: Any, **kwargs: Any) -> MoveResult:
|
|
336
|
+
"""Validate a bridge placement move.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
args[0]: Starting row (1-indexed)
|
|
340
|
+
args[1]: Starting column (1-indexed)
|
|
341
|
+
args[2]: Ending row (1-indexed)
|
|
342
|
+
args[3]: Ending column (1-indexed)
|
|
343
|
+
args[4]: Number of bridges (1 or 2, or 0 to remove)
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
MoveResult containing success status and message
|
|
347
|
+
"""
|
|
348
|
+
if len(args) < 5:
|
|
349
|
+
return MoveResult(success=False, message="Usage: place <row1> <col1> <row2> <col2> <count>")
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
r1, c1, r2, c2, count = int(args[0]) - 1, int(args[1]) - 1, int(args[2]) - 1, int(args[3]) - 1, int(args[4])
|
|
353
|
+
except (ValueError, IndexError):
|
|
354
|
+
return MoveResult(success=False, message="Invalid coordinates or count")
|
|
355
|
+
|
|
356
|
+
# Validate coordinates
|
|
357
|
+
if not (0 <= r1 < self.size and 0 <= c1 < self.size and 0 <= r2 < self.size and 0 <= c2 < self.size):
|
|
358
|
+
return MoveResult(success=False, message="Coordinates out of range")
|
|
359
|
+
|
|
360
|
+
# Check that they're not the same position (before checking if it's an island)
|
|
361
|
+
if r1 == r2 and c1 == c2:
|
|
362
|
+
return MoveResult(success=False, message="Cannot connect island to itself")
|
|
363
|
+
|
|
364
|
+
# Check that both positions are islands
|
|
365
|
+
if self.grid[r1][c1] == 0 or self.grid[r2][c2] == 0:
|
|
366
|
+
return MoveResult(success=False, message="Both positions must be islands")
|
|
367
|
+
|
|
368
|
+
# Check that islands are in a line (horizontal or vertical)
|
|
369
|
+
if r1 != r2 and c1 != c2:
|
|
370
|
+
return MoveResult(success=False, message="Bridges must be horizontal or vertical")
|
|
371
|
+
|
|
372
|
+
# Normalize bridge coordinates
|
|
373
|
+
bridge_key = self._normalize_bridge(r1, c1, r2, c2)
|
|
374
|
+
|
|
375
|
+
# Validate count
|
|
376
|
+
if count < 0 or count > 2:
|
|
377
|
+
return MoveResult(success=False, message="Bridge count must be 0, 1, or 2")
|
|
378
|
+
|
|
379
|
+
# Check for crossing bridges
|
|
380
|
+
if count > 0:
|
|
381
|
+
for (br1, bc1, br2, bc2), bcount in self.bridges.items():
|
|
382
|
+
if bcount > 0 and (br1, bc1, br2, bc2) != bridge_key:
|
|
383
|
+
if self._bridges_cross(r1, c1, r2, c2, br1, bc1, br2, bc2):
|
|
384
|
+
return MoveResult(success=False, message="Bridges cannot cross")
|
|
385
|
+
|
|
386
|
+
# Update or remove bridge
|
|
387
|
+
if count == 0:
|
|
388
|
+
if bridge_key in self.bridges:
|
|
389
|
+
del self.bridges[bridge_key]
|
|
390
|
+
self.moves_made += 1
|
|
391
|
+
return MoveResult(success=True, message="Bridge removed")
|
|
392
|
+
return MoveResult(success=False, message="No bridge to remove")
|
|
393
|
+
else:
|
|
394
|
+
self.bridges[bridge_key] = count
|
|
395
|
+
self.moves_made += 1
|
|
396
|
+
return MoveResult(success=True, message=f"Bridge placed ({count} bridge{'s' if count > 1 else ''})")
|
|
397
|
+
|
|
398
|
+
def is_complete(self) -> bool:
|
|
399
|
+
"""Check if the puzzle is completely and correctly solved."""
|
|
400
|
+
# Check that each island has the correct number of bridges
|
|
401
|
+
for r, c in self.islands:
|
|
402
|
+
required = self.grid[r][c]
|
|
403
|
+
actual = 0
|
|
404
|
+
|
|
405
|
+
for (r1, c1, r2, c2), count in self.bridges.items():
|
|
406
|
+
if (r1, c1) == (r, c) or (r2, c2) == (r, c):
|
|
407
|
+
actual += count
|
|
408
|
+
|
|
409
|
+
if actual != required:
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
# Check that all islands are connected (simplified check)
|
|
413
|
+
# In a full implementation, we'd do a proper connectivity check
|
|
414
|
+
return len(self.bridges) >= len(self.islands) - 1
|
|
415
|
+
|
|
416
|
+
async def get_hint(self) -> tuple[Any, str] | None:
|
|
417
|
+
"""Get a hint for the next move."""
|
|
418
|
+
# Find a bridge in the solution that's not yet placed correctly
|
|
419
|
+
for bridge_key, solution_count in self.solution.items():
|
|
420
|
+
current_count = self.bridges.get(bridge_key, 0)
|
|
421
|
+
if current_count != solution_count:
|
|
422
|
+
r1, c1, r2, c2 = bridge_key
|
|
423
|
+
return (
|
|
424
|
+
(r1 + 1, c1 + 1, r2 + 1, c2 + 1, solution_count),
|
|
425
|
+
f"Try placing {solution_count} bridge(s) between ({r1 + 1},{c1 + 1}) and ({r2 + 1},{c2 + 1})",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
def render_grid(self) -> str:
|
|
431
|
+
"""Render the current puzzle state as ASCII art."""
|
|
432
|
+
lines = []
|
|
433
|
+
|
|
434
|
+
# Header
|
|
435
|
+
header = " |"
|
|
436
|
+
for c in range(self.size):
|
|
437
|
+
header += f" {c + 1:2d}"
|
|
438
|
+
lines.append(header)
|
|
439
|
+
lines.append(" +" + "---" * self.size)
|
|
440
|
+
|
|
441
|
+
# Grid rows
|
|
442
|
+
for r in range(self.size):
|
|
443
|
+
row = f"{r + 1:2d}|"
|
|
444
|
+
for c in range(self.size):
|
|
445
|
+
if self.grid[r][c] > 0:
|
|
446
|
+
# This is an island
|
|
447
|
+
row += f" {self.grid[r][c]:2d}"
|
|
448
|
+
else:
|
|
449
|
+
# Check for bridges
|
|
450
|
+
bridge_char = " ."
|
|
451
|
+
|
|
452
|
+
# Check horizontal bridges
|
|
453
|
+
for (r1, c1, r2, c2), count in self.bridges.items():
|
|
454
|
+
if r1 == r2 == r and min(c1, c2) < c < max(c1, c2):
|
|
455
|
+
bridge_char = " ══" if count == 2 else " ──"
|
|
456
|
+
break
|
|
457
|
+
|
|
458
|
+
# Check vertical bridges
|
|
459
|
+
for (r1, c1, r2, c2), count in self.bridges.items():
|
|
460
|
+
if c1 == c2 == c and min(r1, r2) < r < max(r1, r2):
|
|
461
|
+
bridge_char = " ║ " if count == 2 else " │ "
|
|
462
|
+
break
|
|
463
|
+
|
|
464
|
+
row += bridge_char
|
|
465
|
+
|
|
466
|
+
lines.append(row)
|
|
467
|
+
|
|
468
|
+
return "\n".join(lines)
|
|
469
|
+
|
|
470
|
+
def get_rules(self) -> str:
|
|
471
|
+
"""Get the rules description for this puzzle type."""
|
|
472
|
+
return """BRIDGES (HASHIWOKAKERO) RULES:
|
|
473
|
+
- Connect all islands with bridges
|
|
474
|
+
- Each island shows the number of bridges it needs
|
|
475
|
+
- Bridges can only be horizontal or vertical
|
|
476
|
+
- Bridges cannot cross each other
|
|
477
|
+
- At most 2 bridges can connect any pair of islands
|
|
478
|
+
- All islands must be connected in a single network"""
|
|
479
|
+
|
|
480
|
+
def get_commands(self) -> str:
|
|
481
|
+
"""Get the available commands for this puzzle type."""
|
|
482
|
+
return """BRIDGES COMMANDS:
|
|
483
|
+
place <r1> <c1> <r2> <c2> <count> - Place bridges (1 or 2, or 0 to remove)
|
|
484
|
+
Example: place 1 1 1 3 2 (places 2 bridges between islands)
|
|
485
|
+
hint - Get a hint for the next move
|
|
486
|
+
check - Check if puzzle is complete
|
|
487
|
+
solve - Show the solution
|
|
488
|
+
menu - Return to main menu
|
|
489
|
+
quit - Exit the game"""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Configuration for Einstein game."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ...models.enums import DifficultyLevel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EinsteinConfig(BaseModel):
|
|
9
|
+
"""Configuration for Einstein puzzle game."""
|
|
10
|
+
|
|
11
|
+
difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
|
|
12
|
+
num_clues: int = Field(ge=5, le=15, description="Number of clues provided")
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_difficulty(cls, difficulty: DifficultyLevel) -> "EinsteinConfig":
|
|
16
|
+
"""Create config from difficulty level."""
|
|
17
|
+
config_map = {
|
|
18
|
+
DifficultyLevel.EASY: {"num_clues": 12},
|
|
19
|
+
DifficultyLevel.MEDIUM: {"num_clues": 10},
|
|
20
|
+
DifficultyLevel.HARD: {"num_clues": 8},
|
|
21
|
+
}
|
|
22
|
+
params = config_map[difficulty]
|
|
23
|
+
return cls(difficulty=difficulty, **params)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Einstein's Puzzle constants and static data."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
# Einstein's Puzzle attributes
|
|
6
|
+
COLORS: Final[list[str]] = ["Red", "Green", "Blue", "Yellow", "White"]
|
|
7
|
+
NATIONALITIES: Final[list[str]] = ["British", "Swedish", "Danish", "Norwegian", "German"]
|
|
8
|
+
DRINKS: Final[list[str]] = ["Tea", "Coffee", "Milk", "Beer", "Water"]
|
|
9
|
+
SMOKES: Final[list[str]] = ["Pall Mall", "Dunhill", "Blend", "Blue Master", "Prince"]
|
|
10
|
+
PETS: Final[list[str]] = ["Dog", "Bird", "Cat", "Horse", "Fish"]
|
|
11
|
+
|
|
12
|
+
# Attribute names (for iteration)
|
|
13
|
+
ATTRIBUTES: Final[list[str]] = ["color", "nationality", "drink", "smoke", "pet"]
|