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,386 @@
|
|
|
1
|
+
"""Slitherlink 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 SlitherlinkConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SlitherlinkGame(PuzzleGame):
|
|
11
|
+
"""Slitherlink puzzle game.
|
|
12
|
+
|
|
13
|
+
Draw a single continuous loop by connecting dots.
|
|
14
|
+
Numbers indicate how many edges around that cell are part of the loop.
|
|
15
|
+
The loop must not branch or cross itself.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
|
|
19
|
+
"""Initialize a new Slitherlink game.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
difficulty: Game difficulty level (easy/medium/hard)
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(difficulty, seed, **kwargs)
|
|
25
|
+
|
|
26
|
+
# Grid configuration
|
|
27
|
+
self.config = SlitherlinkConfig.from_difficulty(self.difficulty)
|
|
28
|
+
self.size = self.config.size
|
|
29
|
+
|
|
30
|
+
# Clue grid: -1 = no clue, 0-3 = number of edges
|
|
31
|
+
self.clues = [[-1 for _ in range(self.size)] for _ in range(self.size)]
|
|
32
|
+
|
|
33
|
+
# Edge states: 0 = unknown, 1 = line, 2 = no line (X)
|
|
34
|
+
# Horizontal edges: (size+1) rows x size columns
|
|
35
|
+
self.h_edges = [[0 for _ in range(self.size)] for _ in range(self.size + 1)]
|
|
36
|
+
# Vertical edges: size rows x (size+1) columns
|
|
37
|
+
self.v_edges = [[0 for _ in range(self.size + 1)] for _ in range(self.size)]
|
|
38
|
+
|
|
39
|
+
# Solution edges
|
|
40
|
+
self.solution_h_edges = [[0 for _ in range(self.size)] for _ in range(self.size + 1)]
|
|
41
|
+
self.solution_v_edges = [[0 for _ in range(self.size + 1)] for _ in range(self.size)]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
"""The display name of this puzzle type."""
|
|
46
|
+
return "Slitherlink"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def description(self) -> str:
|
|
50
|
+
"""A one-line description of this puzzle type."""
|
|
51
|
+
return "Draw a single loop - numbers show edge counts"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def constraint_types(self) -> list[str]:
|
|
55
|
+
"""Constraint types demonstrated by this puzzle."""
|
|
56
|
+
return ["global_loop", "local_counting", "topological", "uniqueness"]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def business_analogies(self) -> list[str]:
|
|
60
|
+
"""Business problems this puzzle models."""
|
|
61
|
+
return ["circuit_design", "routing", "loop_detection"]
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def complexity_profile(self) -> dict[str, str]:
|
|
65
|
+
"""Complexity profile of this puzzle."""
|
|
66
|
+
return {"reasoning_type": "deductive", "search_space": "exponential", "constraint_density": "moderate"}
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def optimal_steps(self) -> int | None:
|
|
70
|
+
"""Minimum steps = line segments to draw."""
|
|
71
|
+
if not hasattr(self, "solution_h_edges") or not hasattr(self, "solution_v_edges"):
|
|
72
|
+
return None
|
|
73
|
+
h_lines = sum(sum(row) for row in self.solution_h_edges)
|
|
74
|
+
v_lines = sum(sum(row) for row in self.solution_v_edges)
|
|
75
|
+
return h_lines + v_lines
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def difficulty_profile(self) -> "DifficultyProfile":
|
|
79
|
+
"""Difficulty characteristics for Slitherlink."""
|
|
80
|
+
|
|
81
|
+
logic_depth = {
|
|
82
|
+
DifficultyLevel.EASY.value: 3,
|
|
83
|
+
DifficultyLevel.MEDIUM.value: 5,
|
|
84
|
+
DifficultyLevel.HARD.value: 7,
|
|
85
|
+
}.get(self.difficulty.value, 4)
|
|
86
|
+
return DifficultyProfile(
|
|
87
|
+
logic_depth=logic_depth,
|
|
88
|
+
branching_factor=4.0, # 4 edges per cell
|
|
89
|
+
state_observability=1.0,
|
|
90
|
+
constraint_density=0.6,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _generate_simple_loop(self) -> None:
|
|
94
|
+
"""Generate a simple rectangular loop as the solution."""
|
|
95
|
+
# Create a simple rectangular loop for testing
|
|
96
|
+
# This is a simplified generator - a full implementation would
|
|
97
|
+
# use more sophisticated loop generation algorithms
|
|
98
|
+
|
|
99
|
+
# For a simple version, create a border loop
|
|
100
|
+
for col in range(self.size):
|
|
101
|
+
self.solution_h_edges[0][col] = 1 # Top edge
|
|
102
|
+
self.solution_h_edges[self.size][col] = 1 # Bottom edge
|
|
103
|
+
|
|
104
|
+
for row in range(self.size):
|
|
105
|
+
self.solution_v_edges[row][0] = 1 # Left edge
|
|
106
|
+
self.solution_v_edges[row][self.size] = 1 # Right edge
|
|
107
|
+
|
|
108
|
+
def _count_edges_around_cell(self, row: int, col: int, h_edges: list[list[int]], v_edges: list[list[int]]) -> int:
|
|
109
|
+
"""Count edges around a cell in the given edge configuration.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
row: Cell row
|
|
113
|
+
col: Cell column
|
|
114
|
+
h_edges: Horizontal edges grid
|
|
115
|
+
v_edges: Vertical edges grid
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Number of edges around the cell (0-4)
|
|
119
|
+
"""
|
|
120
|
+
count = 0
|
|
121
|
+
# Top edge
|
|
122
|
+
if h_edges[row][col] == 1:
|
|
123
|
+
count += 1
|
|
124
|
+
# Bottom edge
|
|
125
|
+
if h_edges[row + 1][col] == 1:
|
|
126
|
+
count += 1
|
|
127
|
+
# Left edge
|
|
128
|
+
if v_edges[row][col] == 1:
|
|
129
|
+
count += 1
|
|
130
|
+
# Right edge
|
|
131
|
+
if v_edges[row][col + 1] == 1:
|
|
132
|
+
count += 1
|
|
133
|
+
return count
|
|
134
|
+
|
|
135
|
+
async def generate_puzzle(self) -> None:
|
|
136
|
+
"""Generate a new Slitherlink puzzle."""
|
|
137
|
+
# Generate solution loop
|
|
138
|
+
self._generate_simple_loop()
|
|
139
|
+
|
|
140
|
+
# Generate clues based on solution
|
|
141
|
+
# Place clues based on difficulty
|
|
142
|
+
num_clues_map = {
|
|
143
|
+
DifficultyLevel.EASY: self.size * 2,
|
|
144
|
+
DifficultyLevel.MEDIUM: self.size * 3,
|
|
145
|
+
DifficultyLevel.HARD: self.size * 4,
|
|
146
|
+
}
|
|
147
|
+
num_clues = num_clues_map[self.difficulty]
|
|
148
|
+
|
|
149
|
+
placed = 0
|
|
150
|
+
attempts = 0
|
|
151
|
+
max_attempts = num_clues * 10
|
|
152
|
+
|
|
153
|
+
while placed < num_clues and attempts < max_attempts:
|
|
154
|
+
row = self._rng.randint(0, self.size - 1)
|
|
155
|
+
col = self._rng.randint(0, self.size - 1)
|
|
156
|
+
|
|
157
|
+
if self.clues[row][col] == -1: # No clue yet
|
|
158
|
+
edge_count = self._count_edges_around_cell(row, col, self.solution_h_edges, self.solution_v_edges)
|
|
159
|
+
# Place clue
|
|
160
|
+
self.clues[row][col] = edge_count
|
|
161
|
+
placed += 1
|
|
162
|
+
|
|
163
|
+
attempts += 1
|
|
164
|
+
|
|
165
|
+
# Reset player edges
|
|
166
|
+
self.h_edges = [[0 for _ in range(self.size)] for _ in range(self.size + 1)]
|
|
167
|
+
self.v_edges = [[0 for _ in range(self.size + 1)] for _ in range(self.size)]
|
|
168
|
+
|
|
169
|
+
self.moves_made = 0
|
|
170
|
+
self.game_started = True
|
|
171
|
+
|
|
172
|
+
async def validate_move(self, edge_type: str, row: int, col: int, state: int) -> MoveResult:
|
|
173
|
+
"""Set an edge state.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
edge_type: 'h' for horizontal, 'v' for vertical
|
|
177
|
+
row: Row index (1-indexed, user-facing)
|
|
178
|
+
col: Column index (1-indexed, user-facing)
|
|
179
|
+
state: 0=unknown, 1=line, 2=no line (X)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
MoveResult with success status and message
|
|
183
|
+
"""
|
|
184
|
+
# Convert to 0-indexed
|
|
185
|
+
row -= 1
|
|
186
|
+
col -= 1
|
|
187
|
+
|
|
188
|
+
# Validate state
|
|
189
|
+
if state not in [0, 1, 2]:
|
|
190
|
+
return MoveResult(success=False, message="Invalid state. Use 0=clear, 1=line, 2=X")
|
|
191
|
+
|
|
192
|
+
# Validate edge type and coordinates
|
|
193
|
+
if edge_type.lower() == "h":
|
|
194
|
+
if not (0 <= row <= self.size and 0 <= col < self.size):
|
|
195
|
+
return MoveResult(
|
|
196
|
+
success=False, message=f"Invalid horizontal edge. Row: 1-{self.size + 1}, Col: 1-{self.size}"
|
|
197
|
+
)
|
|
198
|
+
self.h_edges[row][col] = state
|
|
199
|
+
edge_name = "horizontal"
|
|
200
|
+
elif edge_type.lower() == "v":
|
|
201
|
+
if not (0 <= row < self.size and 0 <= col <= self.size):
|
|
202
|
+
return MoveResult(
|
|
203
|
+
success=False, message=f"Invalid vertical edge. Row: 1-{self.size}, Col: 1-{self.size + 1}"
|
|
204
|
+
)
|
|
205
|
+
self.v_edges[row][col] = state
|
|
206
|
+
edge_name = "vertical"
|
|
207
|
+
else:
|
|
208
|
+
return MoveResult(success=False, message="Invalid edge type. Use 'h' or 'v'")
|
|
209
|
+
|
|
210
|
+
self.moves_made += 1
|
|
211
|
+
|
|
212
|
+
state_name = {0: "cleared", 1: "set to line", 2: "marked as X"}[state]
|
|
213
|
+
return MoveResult(
|
|
214
|
+
success=True,
|
|
215
|
+
message=f"{edge_name.capitalize()} edge ({row + 1},{col + 1}) {state_name}",
|
|
216
|
+
state_changed=True,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def is_complete(self) -> bool:
|
|
220
|
+
"""Check if the puzzle is complete and correct."""
|
|
221
|
+
# Check all clues are satisfied
|
|
222
|
+
for row in range(self.size):
|
|
223
|
+
for col in range(self.size):
|
|
224
|
+
if self.clues[row][col] != -1:
|
|
225
|
+
edge_count = self._count_edges_around_cell(row, col, self.h_edges, self.v_edges)
|
|
226
|
+
if edge_count != self.clues[row][col]:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
# Check that we have a valid loop (simplified check)
|
|
230
|
+
# Count total edges - should be > 0 and even
|
|
231
|
+
total_edges = sum(sum(1 for e in row if e == 1) for row in self.h_edges)
|
|
232
|
+
total_edges += sum(sum(1 for e in row if e == 1) for row in self.v_edges)
|
|
233
|
+
|
|
234
|
+
if total_edges == 0:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
# Check each vertex has 0 or 2 edges (no branches)
|
|
238
|
+
for dot_row in range(self.size + 1):
|
|
239
|
+
for dot_col in range(self.size + 1):
|
|
240
|
+
edges = 0
|
|
241
|
+
|
|
242
|
+
# Count edges connected to this dot
|
|
243
|
+
# Horizontal edge to the left (row=dot_row, col=dot_col-1)
|
|
244
|
+
if dot_col > 0:
|
|
245
|
+
if self.h_edges[dot_row][dot_col - 1] == 1:
|
|
246
|
+
edges += 1
|
|
247
|
+
|
|
248
|
+
# Horizontal edge to the right (row=dot_row, col=dot_col)
|
|
249
|
+
if dot_col < self.size:
|
|
250
|
+
if self.h_edges[dot_row][dot_col] == 1:
|
|
251
|
+
edges += 1
|
|
252
|
+
|
|
253
|
+
# Vertical edge above (row=dot_row-1, col=dot_col)
|
|
254
|
+
if dot_row > 0:
|
|
255
|
+
if self.v_edges[dot_row - 1][dot_col] == 1:
|
|
256
|
+
edges += 1
|
|
257
|
+
|
|
258
|
+
# Vertical edge below (row=dot_row, col=dot_col)
|
|
259
|
+
if dot_row < self.size:
|
|
260
|
+
if self.v_edges[dot_row][dot_col] == 1:
|
|
261
|
+
edges += 1
|
|
262
|
+
|
|
263
|
+
# Each vertex must have exactly 0 or 2 edges (part of loop or not)
|
|
264
|
+
if edges != 0 and edges != 2:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
async def get_hint(self) -> tuple[Any, str] | None:
|
|
270
|
+
"""Get a hint for the next move.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Tuple of (hint_data, hint_message) or None
|
|
274
|
+
"""
|
|
275
|
+
# Find an edge that's in the solution but not set by player
|
|
276
|
+
for row in range(self.size + 1):
|
|
277
|
+
for col in range(self.size):
|
|
278
|
+
if self.solution_h_edges[row][col] == 1 and self.h_edges[row][col] != 1:
|
|
279
|
+
hint_data = ("h", row + 1, col + 1, 1)
|
|
280
|
+
hint_message = f"Try setting horizontal edge at ({row + 1},{col + 1}) to line"
|
|
281
|
+
return hint_data, hint_message
|
|
282
|
+
|
|
283
|
+
for row in range(self.size):
|
|
284
|
+
for col in range(self.size + 1):
|
|
285
|
+
if self.solution_v_edges[row][col] == 1 and self.v_edges[row][col] != 1:
|
|
286
|
+
hint_data = ("v", row + 1, col + 1, 1)
|
|
287
|
+
hint_message = f"Try setting vertical edge at ({row + 1},{col + 1}) to line"
|
|
288
|
+
return hint_data, hint_message
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def render_grid(self) -> str:
|
|
293
|
+
"""Render the current puzzle state as ASCII art.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
String representation of the puzzle grid
|
|
297
|
+
"""
|
|
298
|
+
lines = []
|
|
299
|
+
|
|
300
|
+
# Render grid with dots and edges
|
|
301
|
+
for row in range(self.size + 1):
|
|
302
|
+
# Horizontal edges row
|
|
303
|
+
if row <= self.size:
|
|
304
|
+
h_line = " "
|
|
305
|
+
for col in range(self.size):
|
|
306
|
+
h_line += "+"
|
|
307
|
+
# Horizontal edge
|
|
308
|
+
if row < self.size + 1:
|
|
309
|
+
edge = self.h_edges[row][col]
|
|
310
|
+
if edge == 1:
|
|
311
|
+
h_line += "---"
|
|
312
|
+
elif edge == 2:
|
|
313
|
+
h_line += " X "
|
|
314
|
+
else:
|
|
315
|
+
h_line += " "
|
|
316
|
+
h_line += "+"
|
|
317
|
+
lines.append(h_line)
|
|
318
|
+
|
|
319
|
+
# Vertical edges and cells row
|
|
320
|
+
if row < self.size:
|
|
321
|
+
v_line = " "
|
|
322
|
+
for col in range(self.size + 1):
|
|
323
|
+
# Vertical edge
|
|
324
|
+
if col <= self.size:
|
|
325
|
+
edge = self.v_edges[row][col]
|
|
326
|
+
if edge == 1:
|
|
327
|
+
v_line += "|"
|
|
328
|
+
elif edge == 2:
|
|
329
|
+
v_line += "X"
|
|
330
|
+
else:
|
|
331
|
+
v_line += " "
|
|
332
|
+
|
|
333
|
+
# Cell content (clue)
|
|
334
|
+
if col < self.size:
|
|
335
|
+
clue = self.clues[row][col]
|
|
336
|
+
if clue == -1:
|
|
337
|
+
v_line += " "
|
|
338
|
+
else:
|
|
339
|
+
v_line += f" {clue} "
|
|
340
|
+
|
|
341
|
+
lines.append(v_line)
|
|
342
|
+
|
|
343
|
+
return "\n".join(lines)
|
|
344
|
+
|
|
345
|
+
def get_rules(self) -> str:
|
|
346
|
+
"""Get the rules description for Slitherlink.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Multi-line string describing the puzzle rules
|
|
350
|
+
"""
|
|
351
|
+
return """SLITHERLINK RULES:
|
|
352
|
+
- Draw a single continuous loop by connecting dots
|
|
353
|
+
- Numbers show how many edges around that cell are part of the loop
|
|
354
|
+
- The loop must not branch or cross itself
|
|
355
|
+
- Each dot connects to exactly 0 or 2 edges
|
|
356
|
+
- Empty cells have no constraint on edge count"""
|
|
357
|
+
|
|
358
|
+
def get_commands(self) -> str:
|
|
359
|
+
"""Get the available commands for Slitherlink.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Multi-line string describing available commands
|
|
363
|
+
"""
|
|
364
|
+
return """SLITHERLINK COMMANDS:
|
|
365
|
+
set h <row> <col> <state> - Set horizontal edge (e.g., 'set h 1 2 1')
|
|
366
|
+
set v <row> <col> <state> - Set vertical edge (e.g., 'set v 2 1 1')
|
|
367
|
+
state: 0=clear, 1=line, 2=X (not part of loop)
|
|
368
|
+
show - Display current grid
|
|
369
|
+
hint - Get a hint for the next move
|
|
370
|
+
check - Check if puzzle is complete
|
|
371
|
+
solve - Show solution (ends game)
|
|
372
|
+
menu - Return to game selection
|
|
373
|
+
quit - Exit the server"""
|
|
374
|
+
|
|
375
|
+
def get_stats(self) -> str:
|
|
376
|
+
"""Get current game statistics.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
String with game stats
|
|
380
|
+
"""
|
|
381
|
+
total_lines = sum(sum(1 for e in row if e == 1) for row in self.h_edges)
|
|
382
|
+
total_lines += sum(sum(1 for e in row if e == 1) for row in self.v_edges)
|
|
383
|
+
|
|
384
|
+
total_clues = sum(sum(1 for c in row if c != -1) for row in self.clues)
|
|
385
|
+
|
|
386
|
+
return f"Moves made: {self.moves_made} | Lines drawn: {total_lines} | Clues: {total_clues} | Grid: {self.size}×{self.size} | Seed: {self.seed}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Configuration for Sokoban game."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ...models.enums import DifficultyLevel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SokobanConfig(BaseModel):
|
|
9
|
+
"""Configuration for Sokoban 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_boxes: int = Field(ge=2, le=6, description="Number of boxes to push")
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_difficulty(cls, difficulty: DifficultyLevel) -> "SokobanConfig":
|
|
17
|
+
"""Create config from difficulty level."""
|
|
18
|
+
config_map = {
|
|
19
|
+
DifficultyLevel.EASY: {"size": 6, "num_boxes": 2},
|
|
20
|
+
DifficultyLevel.MEDIUM: {"size": 8, "num_boxes": 3},
|
|
21
|
+
DifficultyLevel.HARD: {"size": 10, "num_boxes": 4},
|
|
22
|
+
}
|
|
23
|
+
params = config_map[difficulty]
|
|
24
|
+
return cls(difficulty=difficulty, **params)
|