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,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dataset export utilities for puzzle games.
|
|
3
|
+
|
|
4
|
+
Provides JSONL export for:
|
|
5
|
+
- Training data generation
|
|
6
|
+
- Benchmark dataset creation
|
|
7
|
+
- Episode recording for analysis
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from chuk_puzzles_gym.export.dataset import (
|
|
11
|
+
DatasetExporter,
|
|
12
|
+
export_problems,
|
|
13
|
+
generate_dataset,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DatasetExporter",
|
|
18
|
+
"generate_dataset",
|
|
19
|
+
"export_problems",
|
|
20
|
+
]
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dataset export for puzzle games.
|
|
3
|
+
|
|
4
|
+
Generates JSONL datasets for training and benchmarking.
|
|
5
|
+
Includes step-by-step solution traces for teacher-forcing training.
|
|
6
|
+
|
|
7
|
+
Uses chuk-gym-core's Problem schema and JSONLExporter for consistency
|
|
8
|
+
with chuk-math-gym's export format.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, TextIO
|
|
14
|
+
|
|
15
|
+
from chuk_gym_core import DifficultyLevel, DifficultyProfile, JSONLExporter, Problem
|
|
16
|
+
|
|
17
|
+
from chuk_puzzles_gym.games import AVAILABLE_GAMES
|
|
18
|
+
from chuk_puzzles_gym.games._base import PuzzleGame
|
|
19
|
+
from chuk_puzzles_gym.models import SolverConfig
|
|
20
|
+
from chuk_puzzles_gym.trace import TraceGenerator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DatasetExporter:
|
|
24
|
+
"""
|
|
25
|
+
Export puzzle problems to JSONL format for training.
|
|
26
|
+
|
|
27
|
+
Uses chuk-gym-core's Problem schema and JSONLExporter for
|
|
28
|
+
consistency with chuk-math-gym's export format.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
exporter = DatasetExporter("puzzles.jsonl")
|
|
32
|
+
|
|
33
|
+
# Generate problems for all games
|
|
34
|
+
await exporter.export_all_games(
|
|
35
|
+
count_per_game=100,
|
|
36
|
+
difficulties=[DifficultyLevel.EASY, DifficultyLevel.MEDIUM],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Or specific game
|
|
40
|
+
await exporter.export_game(
|
|
41
|
+
game_name="sudoku",
|
|
42
|
+
count=1000,
|
|
43
|
+
difficulty=DifficultyLevel.HARD,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
exporter.close()
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
output: str | Path | TextIO,
|
|
52
|
+
include_solution: bool = True,
|
|
53
|
+
include_trace: bool = True,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize the exporter.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
output: Output file path or file handle
|
|
60
|
+
include_solution: Whether to include canonical solutions
|
|
61
|
+
include_trace: Whether to include step-by-step traces
|
|
62
|
+
"""
|
|
63
|
+
self.include_solution = include_solution
|
|
64
|
+
self._trace_generator = TraceGenerator()
|
|
65
|
+
|
|
66
|
+
# Use core JSONLExporter for consistent output format
|
|
67
|
+
self._exporter = JSONLExporter(
|
|
68
|
+
output=output,
|
|
69
|
+
include_trace=include_trace,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> "DatasetExporter":
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
76
|
+
self.close()
|
|
77
|
+
|
|
78
|
+
def close(self) -> None:
|
|
79
|
+
"""Close the output file."""
|
|
80
|
+
self._exporter.close()
|
|
81
|
+
|
|
82
|
+
async def export_game(
|
|
83
|
+
self,
|
|
84
|
+
game_name: str,
|
|
85
|
+
count: int,
|
|
86
|
+
difficulty: DifficultyLevel | str = DifficultyLevel.MEDIUM,
|
|
87
|
+
start_seed: int = 0,
|
|
88
|
+
solver_config: SolverConfig | None = None,
|
|
89
|
+
) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Export problems for a specific game.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
game_name: Name of the puzzle game
|
|
95
|
+
count: Number of problems to generate
|
|
96
|
+
difficulty: Difficulty level
|
|
97
|
+
start_seed: Starting seed for reproducibility
|
|
98
|
+
solver_config: Solver configuration
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Number of problems exported
|
|
102
|
+
"""
|
|
103
|
+
if game_name not in AVAILABLE_GAMES:
|
|
104
|
+
raise ValueError(f"Unknown game: {game_name}")
|
|
105
|
+
|
|
106
|
+
game_class = AVAILABLE_GAMES[game_name]
|
|
107
|
+
if isinstance(difficulty, str):
|
|
108
|
+
difficulty = DifficultyLevel(difficulty)
|
|
109
|
+
|
|
110
|
+
exported = 0
|
|
111
|
+
for i in range(count):
|
|
112
|
+
seed = start_seed + i
|
|
113
|
+
|
|
114
|
+
# Create game and generate puzzle (game_class is always a concrete subclass)
|
|
115
|
+
game = game_class( # type: ignore[abstract]
|
|
116
|
+
difficulty=difficulty,
|
|
117
|
+
seed=seed,
|
|
118
|
+
solver_config=solver_config or SolverConfig(),
|
|
119
|
+
)
|
|
120
|
+
await game.generate_puzzle()
|
|
121
|
+
|
|
122
|
+
# Generate solution trace
|
|
123
|
+
trace = self._trace_generator.generate(game)
|
|
124
|
+
|
|
125
|
+
# Convert game to Problem and export using core exporter
|
|
126
|
+
problem = self._game_to_problem(game, seed, difficulty)
|
|
127
|
+
self._exporter.write_problem(problem, trace)
|
|
128
|
+
exported += 1
|
|
129
|
+
|
|
130
|
+
return exported
|
|
131
|
+
|
|
132
|
+
async def export_all_games(
|
|
133
|
+
self,
|
|
134
|
+
count_per_game: int,
|
|
135
|
+
difficulties: list[DifficultyLevel] | None = None,
|
|
136
|
+
start_seed: int = 0,
|
|
137
|
+
games: list[str] | None = None,
|
|
138
|
+
) -> int:
|
|
139
|
+
"""
|
|
140
|
+
Export problems for multiple games.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
count_per_game: Number of problems per game/difficulty combo
|
|
144
|
+
difficulties: Difficulty levels to include (default: all)
|
|
145
|
+
start_seed: Starting seed
|
|
146
|
+
games: Specific games to include (default: all)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Total number of problems exported
|
|
150
|
+
"""
|
|
151
|
+
if difficulties is None:
|
|
152
|
+
difficulties = [
|
|
153
|
+
DifficultyLevel.EASY,
|
|
154
|
+
DifficultyLevel.MEDIUM,
|
|
155
|
+
DifficultyLevel.HARD,
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
game_names = games or list(AVAILABLE_GAMES.keys())
|
|
159
|
+
total = 0
|
|
160
|
+
|
|
161
|
+
for game_name in game_names:
|
|
162
|
+
for difficulty in difficulties:
|
|
163
|
+
count = await self.export_game(
|
|
164
|
+
game_name=game_name,
|
|
165
|
+
count=count_per_game,
|
|
166
|
+
difficulty=difficulty,
|
|
167
|
+
start_seed=start_seed + total,
|
|
168
|
+
)
|
|
169
|
+
total += count
|
|
170
|
+
|
|
171
|
+
return total
|
|
172
|
+
|
|
173
|
+
def _game_to_problem(
|
|
174
|
+
self,
|
|
175
|
+
game: PuzzleGame,
|
|
176
|
+
seed: int,
|
|
177
|
+
difficulty: DifficultyLevel,
|
|
178
|
+
) -> Problem:
|
|
179
|
+
"""Convert a game instance to a chuk-gym-core Problem.
|
|
180
|
+
|
|
181
|
+
Uses the core Problem schema for consistency with chuk-math-gym.
|
|
182
|
+
"""
|
|
183
|
+
domain = game.name.lower().replace(" ", "_")
|
|
184
|
+
profile = game.difficulty_profile
|
|
185
|
+
|
|
186
|
+
# Build gold_answer from canonical solution if available
|
|
187
|
+
gold_answer = None
|
|
188
|
+
if self.include_solution:
|
|
189
|
+
canonical = game.canonical_solution
|
|
190
|
+
if canonical:
|
|
191
|
+
gold_answer = str(canonical)
|
|
192
|
+
|
|
193
|
+
# Create Problem using core schema
|
|
194
|
+
return Problem(
|
|
195
|
+
# Identity
|
|
196
|
+
id=Problem.generate_id(domain, difficulty, seed),
|
|
197
|
+
seed=seed,
|
|
198
|
+
# Classification
|
|
199
|
+
domain=domain,
|
|
200
|
+
difficulty=difficulty,
|
|
201
|
+
# Content
|
|
202
|
+
prompt=f"{game.name}: {game.description}\n\n{game.get_rules()}\n\n{game.render_grid()}",
|
|
203
|
+
initial_state=game.grid if hasattr(game, "grid") else None,
|
|
204
|
+
gold_answer=gold_answer,
|
|
205
|
+
# Constraint metadata
|
|
206
|
+
constraint_types=game.constraint_types,
|
|
207
|
+
business_analogies=game.business_analogies,
|
|
208
|
+
# Difficulty profile
|
|
209
|
+
difficulty_profile=DifficultyProfile(
|
|
210
|
+
logic_depth=profile.logic_depth,
|
|
211
|
+
branching_factor=profile.branching_factor,
|
|
212
|
+
state_observability=profile.state_observability,
|
|
213
|
+
constraint_density=profile.constraint_density,
|
|
214
|
+
),
|
|
215
|
+
# Metadata
|
|
216
|
+
operation_count=game.optimal_steps,
|
|
217
|
+
tags=[domain, difficulty.value],
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def count(self) -> int:
|
|
222
|
+
"""Number of records written."""
|
|
223
|
+
return self._exporter.count
|
|
224
|
+
|
|
225
|
+
def flush(self) -> None:
|
|
226
|
+
"""Flush the output buffer."""
|
|
227
|
+
self._exporter.flush()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def generate_dataset(
|
|
231
|
+
output_path: str | Path,
|
|
232
|
+
games: list[str] | None = None,
|
|
233
|
+
count_per_game: int = 100,
|
|
234
|
+
difficulties: list[str] | None = None,
|
|
235
|
+
start_seed: int = 0,
|
|
236
|
+
include_trace: bool = True,
|
|
237
|
+
) -> int:
|
|
238
|
+
"""
|
|
239
|
+
Generate a complete puzzle dataset.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
output_path: Path to output JSONL file
|
|
243
|
+
games: Games to include (default: all)
|
|
244
|
+
count_per_game: Problems per game/difficulty
|
|
245
|
+
difficulties: Difficulties to include (default: easy, medium, hard)
|
|
246
|
+
start_seed: Starting seed
|
|
247
|
+
include_trace: Whether to include step-by-step traces
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Total number of problems generated
|
|
251
|
+
"""
|
|
252
|
+
diff_levels = None
|
|
253
|
+
if difficulties:
|
|
254
|
+
diff_levels = [DifficultyLevel(d) for d in difficulties]
|
|
255
|
+
|
|
256
|
+
with DatasetExporter(
|
|
257
|
+
output_path,
|
|
258
|
+
include_trace=include_trace,
|
|
259
|
+
) as exporter:
|
|
260
|
+
total = await exporter.export_all_games(
|
|
261
|
+
count_per_game=count_per_game,
|
|
262
|
+
difficulties=diff_levels,
|
|
263
|
+
start_seed=start_seed,
|
|
264
|
+
games=games,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return total
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def export_problems(
|
|
271
|
+
game_name: str,
|
|
272
|
+
count: int,
|
|
273
|
+
output_path: str | Path,
|
|
274
|
+
difficulty: str = "medium",
|
|
275
|
+
start_seed: int = 0,
|
|
276
|
+
) -> int:
|
|
277
|
+
"""
|
|
278
|
+
Export problems for a single game.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
game_name: Name of the puzzle game
|
|
282
|
+
count: Number of problems
|
|
283
|
+
output_path: Path to output JSONL file
|
|
284
|
+
difficulty: Difficulty level
|
|
285
|
+
start_seed: Starting seed
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Number of problems exported
|
|
289
|
+
"""
|
|
290
|
+
with DatasetExporter(output_path) as exporter:
|
|
291
|
+
exported = await exporter.export_game(
|
|
292
|
+
game_name=game_name,
|
|
293
|
+
count=count,
|
|
294
|
+
difficulty=DifficultyLevel(difficulty),
|
|
295
|
+
start_seed=start_seed,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return exported
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main() -> None:
|
|
302
|
+
"""CLI entry point for dataset generation."""
|
|
303
|
+
import argparse
|
|
304
|
+
|
|
305
|
+
parser = argparse.ArgumentParser(description="Generate puzzle datasets for training and benchmarking")
|
|
306
|
+
parser.add_argument(
|
|
307
|
+
"-o",
|
|
308
|
+
"--output",
|
|
309
|
+
default="puzzles.jsonl",
|
|
310
|
+
help="Output file path (default: puzzles.jsonl)",
|
|
311
|
+
)
|
|
312
|
+
parser.add_argument(
|
|
313
|
+
"-g",
|
|
314
|
+
"--games",
|
|
315
|
+
nargs="+",
|
|
316
|
+
help="Games to include (default: all)",
|
|
317
|
+
)
|
|
318
|
+
parser.add_argument(
|
|
319
|
+
"-n",
|
|
320
|
+
"--count",
|
|
321
|
+
type=int,
|
|
322
|
+
default=100,
|
|
323
|
+
help="Problems per game/difficulty (default: 100)",
|
|
324
|
+
)
|
|
325
|
+
parser.add_argument(
|
|
326
|
+
"-d",
|
|
327
|
+
"--difficulties",
|
|
328
|
+
nargs="+",
|
|
329
|
+
default=["easy", "medium", "hard"],
|
|
330
|
+
help="Difficulties to include",
|
|
331
|
+
)
|
|
332
|
+
parser.add_argument(
|
|
333
|
+
"-s",
|
|
334
|
+
"--seed",
|
|
335
|
+
type=int,
|
|
336
|
+
default=0,
|
|
337
|
+
help="Starting seed (default: 0)",
|
|
338
|
+
)
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
"--no-trace",
|
|
341
|
+
action="store_true",
|
|
342
|
+
help="Exclude step-by-step solution traces",
|
|
343
|
+
)
|
|
344
|
+
parser.add_argument(
|
|
345
|
+
"--list-games",
|
|
346
|
+
action="store_true",
|
|
347
|
+
help="List available games and exit",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
args = parser.parse_args()
|
|
351
|
+
|
|
352
|
+
if args.list_games:
|
|
353
|
+
print("Available games:")
|
|
354
|
+
for name in sorted(AVAILABLE_GAMES.keys()):
|
|
355
|
+
game_class = AVAILABLE_GAMES[name]
|
|
356
|
+
# Create instance to get description (game_class is always a concrete subclass)
|
|
357
|
+
game = game_class() # type: ignore[abstract]
|
|
358
|
+
print(f" {name}: {game.description}")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
total = asyncio.run(
|
|
362
|
+
generate_dataset(
|
|
363
|
+
output_path=args.output,
|
|
364
|
+
games=args.games,
|
|
365
|
+
count_per_game=args.count,
|
|
366
|
+
difficulties=args.difficulties,
|
|
367
|
+
start_seed=args.seed,
|
|
368
|
+
include_trace=not args.no_trace,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
print(f"Generated {total} problems -> {args.output}")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Puzzle game implementations."""
|
|
2
|
+
|
|
3
|
+
from .binary import BinaryPuzzleGame
|
|
4
|
+
from .bridges import BridgesGame
|
|
5
|
+
from .einstein import EinsteinGame
|
|
6
|
+
from .fillomino import FillominoGame
|
|
7
|
+
from .futoshiki import FutoshikiGame
|
|
8
|
+
from .hidato import HidatoGame
|
|
9
|
+
from .hitori import HitoriGame
|
|
10
|
+
from .kakuro import KakuroGame
|
|
11
|
+
from .kenken import KenKenGame
|
|
12
|
+
from .killer_sudoku import KillerSudokuGame
|
|
13
|
+
from .knapsack import KnapsackGame
|
|
14
|
+
from .lights_out import LightsOutGame
|
|
15
|
+
from .logic_grid import LogicGridGame
|
|
16
|
+
from .mastermind import MastermindGame
|
|
17
|
+
from .minesweeper import MinesweeperGame
|
|
18
|
+
from .nonogram import NonogramGame
|
|
19
|
+
from .nurikabe import NurikabeGame
|
|
20
|
+
from .scheduler import SchedulerGame
|
|
21
|
+
from .shikaku import ShikakuGame
|
|
22
|
+
from .slitherlink import SlitherlinkGame
|
|
23
|
+
from .sokoban import SokobanGame
|
|
24
|
+
from .star_battle import StarBattleGame
|
|
25
|
+
from .sudoku import SudokuGame
|
|
26
|
+
from .sudoku.commands import SudokuCommandHandler
|
|
27
|
+
from .tents import TentsGame
|
|
28
|
+
|
|
29
|
+
# Registry of available games
|
|
30
|
+
AVAILABLE_GAMES = {
|
|
31
|
+
# Classic Logic Puzzles
|
|
32
|
+
"sudoku": SudokuGame,
|
|
33
|
+
"kenken": KenKenGame,
|
|
34
|
+
"kakuro": KakuroGame,
|
|
35
|
+
"binary": BinaryPuzzleGame,
|
|
36
|
+
"futoshiki": FutoshikiGame,
|
|
37
|
+
"nonogram": NonogramGame,
|
|
38
|
+
"logic": LogicGridGame,
|
|
39
|
+
# Advanced CP-SAT Puzzles
|
|
40
|
+
"killer": KillerSudokuGame,
|
|
41
|
+
"lights": LightsOutGame,
|
|
42
|
+
"mastermind": MastermindGame,
|
|
43
|
+
"slither": SlitherlinkGame,
|
|
44
|
+
"bridges": BridgesGame,
|
|
45
|
+
"hitori": HitoriGame,
|
|
46
|
+
"shikaku": ShikakuGame,
|
|
47
|
+
# Specialized Constraint Puzzles
|
|
48
|
+
"hidato": HidatoGame,
|
|
49
|
+
"tents": TentsGame,
|
|
50
|
+
"fillomino": FillominoGame,
|
|
51
|
+
"star_battle": StarBattleGame,
|
|
52
|
+
"sokoban": SokobanGame,
|
|
53
|
+
# Optimization Challenges
|
|
54
|
+
"knapsack": KnapsackGame,
|
|
55
|
+
"scheduler": SchedulerGame,
|
|
56
|
+
# Advanced Reasoning
|
|
57
|
+
"nurikabe": NurikabeGame,
|
|
58
|
+
"einstein": EinsteinGame,
|
|
59
|
+
"minesweeper": MinesweeperGame,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Registry of game command handlers (games that have moved command handling out of server)
|
|
63
|
+
GAME_COMMAND_HANDLERS = {
|
|
64
|
+
"sudoku": SudokuCommandHandler,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"SudokuGame",
|
|
69
|
+
"KenKenGame",
|
|
70
|
+
"KakuroGame",
|
|
71
|
+
"BinaryPuzzleGame",
|
|
72
|
+
"BridgesGame",
|
|
73
|
+
"FillominoGame",
|
|
74
|
+
"FutoshikiGame",
|
|
75
|
+
"HidatoGame",
|
|
76
|
+
"HitoriGame",
|
|
77
|
+
"NonogramGame",
|
|
78
|
+
"LogicGridGame",
|
|
79
|
+
"KillerSudokuGame",
|
|
80
|
+
"LightsOutGame",
|
|
81
|
+
"MastermindGame",
|
|
82
|
+
"ShikakuGame",
|
|
83
|
+
"SlitherlinkGame",
|
|
84
|
+
"SokobanGame",
|
|
85
|
+
"StarBattleGame",
|
|
86
|
+
"TentsGame",
|
|
87
|
+
"KnapsackGame",
|
|
88
|
+
"SchedulerGame",
|
|
89
|
+
"NurikabeGame",
|
|
90
|
+
"EinsteinGame",
|
|
91
|
+
"MinesweeperGame",
|
|
92
|
+
"AVAILABLE_GAMES",
|
|
93
|
+
"GAME_COMMAND_HANDLERS",
|
|
94
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Abstract base class for game command handlers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ...models import GameCommand, MoveResult
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .game import PuzzleGame
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CommandResult:
|
|
15
|
+
"""Result of executing a game command."""
|
|
16
|
+
|
|
17
|
+
result: MoveResult
|
|
18
|
+
should_display: bool = True
|
|
19
|
+
is_game_over: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GameCommandHandler(ABC):
|
|
23
|
+
"""Abstract base class for handling game-specific commands.
|
|
24
|
+
|
|
25
|
+
Each game implements its own command handler that knows how to:
|
|
26
|
+
- Parse command arguments
|
|
27
|
+
- Validate and execute moves
|
|
28
|
+
- Return appropriate results
|
|
29
|
+
|
|
30
|
+
This decouples game logic from server routing.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, game: "PuzzleGame"):
|
|
34
|
+
"""Initialize with the game instance.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
game: The puzzle game instance to handle commands for
|
|
38
|
+
"""
|
|
39
|
+
self.game = game
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def supported_commands(self) -> set[GameCommand]:
|
|
44
|
+
"""Return the set of GameCommand enums this handler supports.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Set of GameCommand values this game responds to
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
async def handle_command(self, cmd: GameCommand, args: list[str]) -> CommandResult:
|
|
53
|
+
"""Handle a game-specific command.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
cmd: The GameCommand enum value
|
|
57
|
+
args: List of string arguments (already split from input)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
CommandResult with the move result and display flags
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def parse_int(self, value: str, name: str) -> int | None:
|
|
65
|
+
"""Helper to parse an integer argument.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
value: String value to parse
|
|
69
|
+
name: Name of the argument (for error messages)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Parsed integer or None if invalid
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
return int(value)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def error_result(self, message: str) -> CommandResult:
|
|
80
|
+
"""Create an error CommandResult.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
message: Error message to display
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
CommandResult with failed MoveResult
|
|
87
|
+
"""
|
|
88
|
+
return CommandResult(
|
|
89
|
+
result=MoveResult(success=False, message=message),
|
|
90
|
+
should_display=False,
|
|
91
|
+
)
|