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,726 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace generator for puzzle games.
|
|
3
|
+
|
|
4
|
+
Generates step-by-step solution traces compatible with chuk-gym-core.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from chuk_gym_core import Step, StepOperation, Trace
|
|
10
|
+
|
|
11
|
+
from chuk_puzzles_gym.games._base import PuzzleGame
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TraceGenerator:
|
|
15
|
+
"""
|
|
16
|
+
Generates solution traces for puzzle games.
|
|
17
|
+
|
|
18
|
+
Traces are machine-verifiable sequences of steps that transform
|
|
19
|
+
the initial puzzle state into the solved state.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
game = SudokuGame(difficulty="medium", seed=42)
|
|
23
|
+
await game.generate_puzzle()
|
|
24
|
+
|
|
25
|
+
generator = TraceGenerator()
|
|
26
|
+
trace = generator.generate(game)
|
|
27
|
+
|
|
28
|
+
# Trace contains step-by-step solution
|
|
29
|
+
for step in trace.steps:
|
|
30
|
+
print(f"{step.explanation}")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def generate(self, game: PuzzleGame) -> Trace:
|
|
34
|
+
"""
|
|
35
|
+
Generate a solution trace for a puzzle game.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
game: A puzzle game instance with a generated puzzle
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Trace with step-by-step solution
|
|
42
|
+
"""
|
|
43
|
+
# Normalize game name: lowercase, spaces to underscores, remove special chars
|
|
44
|
+
game_name = game.name.lower().replace(" ", "_")
|
|
45
|
+
# Remove apostrophes and other non-alphanumeric chars (except underscore)
|
|
46
|
+
game_name = "".join(c for c in game_name if c.isalnum() or c == "_")
|
|
47
|
+
|
|
48
|
+
# Dispatch to game-specific generator
|
|
49
|
+
if hasattr(self, f"_generate_{game_name}"):
|
|
50
|
+
return getattr(self, f"_generate_{game_name}")(game)
|
|
51
|
+
|
|
52
|
+
# Fall back to generic grid-based generator
|
|
53
|
+
if hasattr(game, "grid") and hasattr(game, "solution"):
|
|
54
|
+
return self._generate_grid_puzzle(game)
|
|
55
|
+
|
|
56
|
+
# Last resort: generate from canonical solution
|
|
57
|
+
if game.canonical_solution:
|
|
58
|
+
return self._generate_from_canonical(game)
|
|
59
|
+
|
|
60
|
+
# Empty trace if nothing else works
|
|
61
|
+
return Trace(
|
|
62
|
+
problem_id=f"{game_name}_{game.difficulty.value}_{game.seed}",
|
|
63
|
+
steps=[],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _generate_grid_puzzle(self, game: PuzzleGame) -> Trace:
|
|
67
|
+
"""Generate trace for a grid-based puzzle with solution."""
|
|
68
|
+
problem_id = f"{game.name.lower().replace(' ', '_')}_{game.difficulty.value}_{game.seed}"
|
|
69
|
+
steps: list[Step] = []
|
|
70
|
+
|
|
71
|
+
# Get initial and solution grids (dynamically accessed attributes)
|
|
72
|
+
grid = getattr(game, "grid", None)
|
|
73
|
+
initial = getattr(game, "initial_grid", grid)
|
|
74
|
+
solution = getattr(game, "solution", None)
|
|
75
|
+
|
|
76
|
+
if solution is None:
|
|
77
|
+
return Trace(problem_id=problem_id, steps=[])
|
|
78
|
+
|
|
79
|
+
# Find all cells that need to be filled
|
|
80
|
+
# Handle both 0 and -1 as empty values (different games use different conventions)
|
|
81
|
+
moves: list[tuple[int, int, Any]] = []
|
|
82
|
+
for row in range(len(solution)):
|
|
83
|
+
for col in range(len(solution[row])):
|
|
84
|
+
initial_val = initial[row][col]
|
|
85
|
+
solution_val = solution[row][col]
|
|
86
|
+
# Check if cell needs to be filled: initial is empty (0 or -1) and differs from solution
|
|
87
|
+
is_empty = initial_val == 0 or initial_val == -1
|
|
88
|
+
needs_fill = is_empty and solution_val != initial_val
|
|
89
|
+
if needs_fill:
|
|
90
|
+
moves.append((row, col, solution_val))
|
|
91
|
+
|
|
92
|
+
# Generate steps for each move
|
|
93
|
+
for i, (row, col, value) in enumerate(moves):
|
|
94
|
+
step = Step(
|
|
95
|
+
index=i,
|
|
96
|
+
operation=StepOperation.PLACE,
|
|
97
|
+
before_state=self._format_cell_state(row, col, None, is_empty=True),
|
|
98
|
+
after_state=self._format_cell_state(row, col, value, is_empty=False),
|
|
99
|
+
output_value=value,
|
|
100
|
+
position=(row + 1, col + 1), # 1-indexed for user display
|
|
101
|
+
rule_applied=self._infer_rule(game, row, col, value),
|
|
102
|
+
explanation=self._generate_explanation(game, row, col, value),
|
|
103
|
+
)
|
|
104
|
+
steps.append(step)
|
|
105
|
+
|
|
106
|
+
return Trace(
|
|
107
|
+
problem_id=problem_id,
|
|
108
|
+
steps=steps,
|
|
109
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _generate_from_canonical(self, game: PuzzleGame) -> Trace:
|
|
113
|
+
"""Generate trace from canonical solution list."""
|
|
114
|
+
problem_id = f"{game.name.lower().replace(' ', '_')}_{game.difficulty.value}_{game.seed}"
|
|
115
|
+
steps: list[Step] = []
|
|
116
|
+
|
|
117
|
+
canonical = game.canonical_solution
|
|
118
|
+
if not canonical:
|
|
119
|
+
return Trace(problem_id=problem_id, steps=[])
|
|
120
|
+
|
|
121
|
+
for i, move in enumerate(canonical):
|
|
122
|
+
if isinstance(move, tuple) and len(move) >= 3:
|
|
123
|
+
row, col, value = move[0], move[1], move[2]
|
|
124
|
+
step = Step(
|
|
125
|
+
index=i,
|
|
126
|
+
operation=StepOperation.PLACE,
|
|
127
|
+
before_state=f"cell({row},{col})=empty",
|
|
128
|
+
after_state=f"cell({row},{col})={value}",
|
|
129
|
+
output_value=value,
|
|
130
|
+
position=(row, col),
|
|
131
|
+
explanation=f"Place {value} at position ({row}, {col})",
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
step = Step(
|
|
135
|
+
index=i,
|
|
136
|
+
operation=StepOperation.DEDUCE,
|
|
137
|
+
before_state=str(move),
|
|
138
|
+
after_state=str(move),
|
|
139
|
+
explanation=f"Move {i + 1}: {move}",
|
|
140
|
+
)
|
|
141
|
+
steps.append(step)
|
|
142
|
+
|
|
143
|
+
return Trace(problem_id=problem_id, steps=steps)
|
|
144
|
+
|
|
145
|
+
def _format_cell_state(self, row: int, col: int, value: Any, is_empty: bool = False) -> str:
|
|
146
|
+
"""Format cell state for step display."""
|
|
147
|
+
if is_empty or value is None or value == -1:
|
|
148
|
+
return f"cell(r{row + 1},c{col + 1})=empty"
|
|
149
|
+
return f"cell(r{row + 1},c{col + 1})={value}"
|
|
150
|
+
|
|
151
|
+
def _infer_rule(self, game: PuzzleGame, row: int, col: int, value: Any) -> str:
|
|
152
|
+
"""Infer the logical rule used to determine this value."""
|
|
153
|
+
game_name = game.name.lower()
|
|
154
|
+
|
|
155
|
+
# Game-specific rules
|
|
156
|
+
if "sudoku" in game_name:
|
|
157
|
+
return self._infer_sudoku_rule(game, row, col, value)
|
|
158
|
+
elif "kenken" in game_name or "kakuro" in game_name:
|
|
159
|
+
return "arithmetic_constraint"
|
|
160
|
+
elif "nonogram" in game_name:
|
|
161
|
+
return "line_constraint"
|
|
162
|
+
elif "binary" in game_name:
|
|
163
|
+
return "balance_constraint"
|
|
164
|
+
|
|
165
|
+
return "constraint_propagation"
|
|
166
|
+
|
|
167
|
+
def _infer_sudoku_rule(self, game: PuzzleGame, row: int, col: int, value: Any) -> str:
|
|
168
|
+
"""Infer the Sudoku-specific rule used."""
|
|
169
|
+
# Get solution grid dynamically
|
|
170
|
+
solution = getattr(game, "solution", None)
|
|
171
|
+
if solution is None:
|
|
172
|
+
return "elimination"
|
|
173
|
+
|
|
174
|
+
# Check if this is the only candidate in the row
|
|
175
|
+
row_vals = set(solution[row]) - {0}
|
|
176
|
+
if len(row_vals) == 9:
|
|
177
|
+
return "naked_single_row"
|
|
178
|
+
|
|
179
|
+
# Check column
|
|
180
|
+
col_vals = {solution[r][col] for r in range(9)} - {0}
|
|
181
|
+
if len(col_vals) == 9:
|
|
182
|
+
return "naked_single_column"
|
|
183
|
+
|
|
184
|
+
# Check box
|
|
185
|
+
box_row, box_col = 3 * (row // 3), 3 * (col // 3)
|
|
186
|
+
box_vals = set()
|
|
187
|
+
for r in range(box_row, box_row + 3):
|
|
188
|
+
for c in range(box_col, box_col + 3):
|
|
189
|
+
if solution[r][c] != 0:
|
|
190
|
+
box_vals.add(solution[r][c])
|
|
191
|
+
if len(box_vals) == 9:
|
|
192
|
+
return "naked_single_box"
|
|
193
|
+
|
|
194
|
+
return "elimination"
|
|
195
|
+
|
|
196
|
+
def _generate_explanation(self, game: PuzzleGame, row: int, col: int, value: Any) -> str:
|
|
197
|
+
"""Generate a natural language explanation for the step."""
|
|
198
|
+
game_name = game.name.lower()
|
|
199
|
+
|
|
200
|
+
if "sudoku" in game_name:
|
|
201
|
+
box_num = 3 * (row // 3) + (col // 3) + 1
|
|
202
|
+
return (
|
|
203
|
+
f"Place {value} at row {row + 1}, column {col + 1}. "
|
|
204
|
+
f"This is the only valid digit for this cell considering "
|
|
205
|
+
f"row {row + 1}, column {col + 1}, and box {box_num} constraints."
|
|
206
|
+
)
|
|
207
|
+
elif "binary" in game_name:
|
|
208
|
+
v = "1" if value == 1 else "0"
|
|
209
|
+
return (
|
|
210
|
+
f"Place {v} at row {row + 1}, column {col + 1}. "
|
|
211
|
+
f"This maintains the balance and avoids three consecutive same digits."
|
|
212
|
+
)
|
|
213
|
+
elif "futoshiki" in game_name:
|
|
214
|
+
return f"Place {value} at row {row + 1}, column {col + 1}. This satisfies the inequality constraints."
|
|
215
|
+
elif "kenken" in game_name:
|
|
216
|
+
return f"Place {value} at row {row + 1}, column {col + 1}. This satisfies the cage arithmetic constraint."
|
|
217
|
+
else:
|
|
218
|
+
return f"Place {value} at row {row + 1}, column {col + 1}."
|
|
219
|
+
|
|
220
|
+
def _identify_checkpoints(self, steps: list[Step]) -> list[int]:
|
|
221
|
+
"""Identify key milestone steps for partial credit."""
|
|
222
|
+
if not steps:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
checkpoints = []
|
|
226
|
+
total = len(steps)
|
|
227
|
+
|
|
228
|
+
# Add checkpoints at 25%, 50%, 75%, 100%
|
|
229
|
+
for pct in [0.25, 0.5, 0.75, 1.0]:
|
|
230
|
+
idx = min(int(total * pct), total - 1)
|
|
231
|
+
if idx not in checkpoints:
|
|
232
|
+
checkpoints.append(idx)
|
|
233
|
+
|
|
234
|
+
return checkpoints
|
|
235
|
+
|
|
236
|
+
# Game-specific generators
|
|
237
|
+
|
|
238
|
+
def _generate_sudoku(self, game: PuzzleGame) -> Trace:
|
|
239
|
+
"""Generate trace specifically for Sudoku."""
|
|
240
|
+
return self._generate_grid_puzzle(game)
|
|
241
|
+
|
|
242
|
+
def _generate_binary(self, game: PuzzleGame) -> Trace:
|
|
243
|
+
"""Generate trace for Binary puzzle."""
|
|
244
|
+
return self._generate_grid_puzzle(game)
|
|
245
|
+
|
|
246
|
+
def _generate_futoshiki(self, game: PuzzleGame) -> Trace:
|
|
247
|
+
"""Generate trace for Futoshiki puzzle."""
|
|
248
|
+
return self._generate_grid_puzzle(game)
|
|
249
|
+
|
|
250
|
+
def _generate_kenken(self, game: PuzzleGame) -> Trace:
|
|
251
|
+
"""Generate trace for KenKen puzzle."""
|
|
252
|
+
return self._generate_grid_puzzle(game)
|
|
253
|
+
|
|
254
|
+
def _generate_kakuro(self, game: PuzzleGame) -> Trace:
|
|
255
|
+
"""Generate trace for Kakuro puzzle."""
|
|
256
|
+
return self._generate_grid_puzzle(game)
|
|
257
|
+
|
|
258
|
+
def _generate_nonogram(self, game: PuzzleGame) -> Trace:
|
|
259
|
+
"""Generate trace for Nonogram puzzle."""
|
|
260
|
+
problem_id = f"nonogram_{game.difficulty.value}_{game.seed}"
|
|
261
|
+
steps: list[Step] = []
|
|
262
|
+
|
|
263
|
+
if hasattr(game, "grid") and hasattr(game, "solution"):
|
|
264
|
+
initial = game.grid
|
|
265
|
+
solution = game.solution
|
|
266
|
+
|
|
267
|
+
step_idx = 0
|
|
268
|
+
for row in range(len(solution)):
|
|
269
|
+
for col in range(len(solution[row])):
|
|
270
|
+
if solution[row][col] != initial[row][col]:
|
|
271
|
+
is_filled = solution[row][col] == 1
|
|
272
|
+
step = Step(
|
|
273
|
+
index=step_idx,
|
|
274
|
+
operation=StepOperation.PLACE if is_filled else StepOperation.ELIMINATE,
|
|
275
|
+
before_state=f"cell(r{row + 1},c{col + 1})=unknown",
|
|
276
|
+
after_state=f"cell(r{row + 1},c{col + 1})={'filled' if is_filled else 'empty'}",
|
|
277
|
+
output_value=is_filled,
|
|
278
|
+
position=(row + 1, col + 1),
|
|
279
|
+
rule_applied="line_logic",
|
|
280
|
+
explanation=f"{'Fill' if is_filled else 'Mark empty'} cell at row {row + 1}, column {col + 1} based on row/column clues.",
|
|
281
|
+
)
|
|
282
|
+
steps.append(step)
|
|
283
|
+
step_idx += 1
|
|
284
|
+
|
|
285
|
+
return Trace(
|
|
286
|
+
problem_id=problem_id,
|
|
287
|
+
steps=steps,
|
|
288
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _generate_mastermind(self, game: PuzzleGame) -> Trace:
|
|
292
|
+
"""Generate trace for Mastermind (code-breaking)."""
|
|
293
|
+
problem_id = f"mastermind_{game.difficulty.value}_{game.seed}"
|
|
294
|
+
steps: list[Step] = []
|
|
295
|
+
|
|
296
|
+
if hasattr(game, "secret_code") and game.secret_code:
|
|
297
|
+
# For Mastermind, the solution is discovering the code
|
|
298
|
+
secret = game.secret_code
|
|
299
|
+
code_str = " ".join(str(c) for c in secret)
|
|
300
|
+
step = Step(
|
|
301
|
+
index=0,
|
|
302
|
+
operation=StepOperation.DEDUCE,
|
|
303
|
+
before_state="secret_code=unknown",
|
|
304
|
+
after_state=f"secret_code={code_str}",
|
|
305
|
+
output_value=secret,
|
|
306
|
+
rule_applied="deductive_elimination",
|
|
307
|
+
explanation=f"The secret code is {code_str}. Discovered through systematic guessing and feedback analysis.",
|
|
308
|
+
)
|
|
309
|
+
steps.append(step)
|
|
310
|
+
|
|
311
|
+
return Trace(problem_id=problem_id, steps=steps)
|
|
312
|
+
|
|
313
|
+
def _generate_einstein(self, game: PuzzleGame) -> Trace:
|
|
314
|
+
"""Generate trace for Einstein puzzle."""
|
|
315
|
+
problem_id = f"einstein_{game.difficulty.value}_{game.seed}"
|
|
316
|
+
steps: list[Step] = []
|
|
317
|
+
|
|
318
|
+
if hasattr(game, "solution") and game.solution:
|
|
319
|
+
# Einstein has assignments: list of HouseAssignment objects
|
|
320
|
+
solution = game.solution
|
|
321
|
+
step_idx = 0
|
|
322
|
+
attributes = ["color", "nationality", "drink", "smoke", "pet"]
|
|
323
|
+
|
|
324
|
+
for house_idx, house_data in enumerate(solution):
|
|
325
|
+
for attr in attributes:
|
|
326
|
+
value = getattr(house_data, attr, None)
|
|
327
|
+
if value:
|
|
328
|
+
step = Step(
|
|
329
|
+
index=step_idx,
|
|
330
|
+
operation=StepOperation.DEDUCE,
|
|
331
|
+
before_state=f"house{house_idx + 1}.{attr}=unknown",
|
|
332
|
+
after_state=f"house{house_idx + 1}.{attr}={value}",
|
|
333
|
+
output_value=value,
|
|
334
|
+
position=(house_idx + 1,),
|
|
335
|
+
rule_applied="logical_deduction",
|
|
336
|
+
explanation=f"House {house_idx + 1} has {attr}={value}, deduced from the clues.",
|
|
337
|
+
)
|
|
338
|
+
steps.append(step)
|
|
339
|
+
step_idx += 1
|
|
340
|
+
|
|
341
|
+
return Trace(
|
|
342
|
+
problem_id=problem_id,
|
|
343
|
+
steps=steps,
|
|
344
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def _generate_logic(self, game: PuzzleGame) -> Trace:
|
|
348
|
+
"""Generate trace for Logic Grid puzzle."""
|
|
349
|
+
problem_id = f"logic_grid_{game.difficulty.value}_{game.seed}"
|
|
350
|
+
steps: list[Step] = []
|
|
351
|
+
|
|
352
|
+
if hasattr(game, "solution") and game.solution:
|
|
353
|
+
# Logic Grid has solution: dict[person -> PersonAttributes]
|
|
354
|
+
solution = game.solution
|
|
355
|
+
step_idx = 0
|
|
356
|
+
attributes = ["color", "pet", "drink"]
|
|
357
|
+
|
|
358
|
+
for person, attrs in solution.items():
|
|
359
|
+
for category in attributes:
|
|
360
|
+
value = getattr(attrs, category, None)
|
|
361
|
+
if value:
|
|
362
|
+
step = Step(
|
|
363
|
+
index=step_idx,
|
|
364
|
+
operation=StepOperation.DEDUCE,
|
|
365
|
+
before_state=f"{person}.{category}=unknown",
|
|
366
|
+
after_state=f"{person}.{category}={value}",
|
|
367
|
+
output_value=value,
|
|
368
|
+
rule_applied="logical_elimination",
|
|
369
|
+
explanation=f"{person} is associated with {category}={value}.",
|
|
370
|
+
)
|
|
371
|
+
steps.append(step)
|
|
372
|
+
step_idx += 1
|
|
373
|
+
|
|
374
|
+
return Trace(
|
|
375
|
+
problem_id=problem_id,
|
|
376
|
+
steps=steps,
|
|
377
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def _generate_logic_grid(self, game: PuzzleGame) -> Trace:
|
|
381
|
+
"""Generate trace for Logic Grid puzzle (alias for _generate_logic)."""
|
|
382
|
+
return self._generate_logic(game)
|
|
383
|
+
|
|
384
|
+
def _generate_hitori(self, game: PuzzleGame) -> Trace:
|
|
385
|
+
"""Generate trace for Hitori puzzle."""
|
|
386
|
+
problem_id = f"hitori_{game.difficulty.value}_{game.seed}"
|
|
387
|
+
steps: list[Step] = []
|
|
388
|
+
|
|
389
|
+
if hasattr(game, "solution") and game.solution:
|
|
390
|
+
# Hitori has solution: 2D bool grid (True = shaded)
|
|
391
|
+
solution = game.solution
|
|
392
|
+
step_idx = 0
|
|
393
|
+
|
|
394
|
+
for row in range(len(solution)):
|
|
395
|
+
for col in range(len(solution[row])):
|
|
396
|
+
if solution[row][col]: # Cell should be shaded
|
|
397
|
+
step = Step(
|
|
398
|
+
index=step_idx,
|
|
399
|
+
operation=StepOperation.ELIMINATE,
|
|
400
|
+
before_state=f"cell(r{row + 1},c{col + 1})=unshaded",
|
|
401
|
+
after_state=f"cell(r{row + 1},c{col + 1})=shaded",
|
|
402
|
+
output_value=True,
|
|
403
|
+
position=(row + 1, col + 1),
|
|
404
|
+
rule_applied="duplicate_elimination",
|
|
405
|
+
explanation=f"Shade cell at row {row + 1}, column {col + 1} to eliminate duplicate.",
|
|
406
|
+
)
|
|
407
|
+
steps.append(step)
|
|
408
|
+
step_idx += 1
|
|
409
|
+
|
|
410
|
+
return Trace(
|
|
411
|
+
problem_id=problem_id,
|
|
412
|
+
steps=steps,
|
|
413
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def _generate_bridges(self, game: PuzzleGame) -> Trace:
|
|
417
|
+
"""Generate trace for Bridges puzzle."""
|
|
418
|
+
problem_id = f"bridges_{game.difficulty.value}_{game.seed}"
|
|
419
|
+
steps: list[Step] = []
|
|
420
|
+
|
|
421
|
+
if hasattr(game, "solution") and game.solution:
|
|
422
|
+
# Bridges has solution: dict[(r1,c1,r2,c2) -> bridge_count]
|
|
423
|
+
solution = game.solution
|
|
424
|
+
step_idx = 0
|
|
425
|
+
|
|
426
|
+
for (r1, c1, r2, c2), count in solution.items():
|
|
427
|
+
if count > 0:
|
|
428
|
+
step = Step(
|
|
429
|
+
index=step_idx,
|
|
430
|
+
operation=StepOperation.PLACE,
|
|
431
|
+
before_state=f"bridge({r1 + 1},{c1 + 1})-({r2 + 1},{c2 + 1})=0",
|
|
432
|
+
after_state=f"bridge({r1 + 1},{c1 + 1})-({r2 + 1},{c2 + 1})={count}",
|
|
433
|
+
output_value=count,
|
|
434
|
+
position=(r1 + 1, c1 + 1, r2 + 1, c2 + 1),
|
|
435
|
+
rule_applied="connectivity_constraint",
|
|
436
|
+
explanation=f"Place {count} bridge(s) between islands at ({r1 + 1},{c1 + 1}) and ({r2 + 1},{c2 + 1}).",
|
|
437
|
+
)
|
|
438
|
+
steps.append(step)
|
|
439
|
+
step_idx += 1
|
|
440
|
+
|
|
441
|
+
return Trace(
|
|
442
|
+
problem_id=problem_id,
|
|
443
|
+
steps=steps,
|
|
444
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def _generate_knapsack(self, game: PuzzleGame) -> Trace:
|
|
448
|
+
"""Generate trace for Knapsack puzzle."""
|
|
449
|
+
problem_id = f"knapsack_{game.difficulty.value}_{game.seed}"
|
|
450
|
+
steps: list[Step] = []
|
|
451
|
+
|
|
452
|
+
if hasattr(game, "optimal_selection") and game.optimal_selection:
|
|
453
|
+
# Knapsack has optimal_selection: list[bool]
|
|
454
|
+
items = getattr(game, "items", [])
|
|
455
|
+
step_idx = 0
|
|
456
|
+
|
|
457
|
+
for i, selected in enumerate(game.optimal_selection):
|
|
458
|
+
if selected:
|
|
459
|
+
item_name = items[i].name if i < len(items) else f"Item {i + 1}"
|
|
460
|
+
item_weight = items[i].weight if i < len(items) else 0
|
|
461
|
+
item_value = items[i].value if i < len(items) else 0
|
|
462
|
+
step = Step(
|
|
463
|
+
index=step_idx,
|
|
464
|
+
operation=StepOperation.PLACE,
|
|
465
|
+
before_state=f"item({item_name})=not_selected",
|
|
466
|
+
after_state=f"item({item_name})=selected",
|
|
467
|
+
output_value=True,
|
|
468
|
+
position=(i + 1,),
|
|
469
|
+
rule_applied="optimization",
|
|
470
|
+
explanation=f"Select {item_name} (weight: {item_weight}, value: {item_value}) for optimal value.",
|
|
471
|
+
)
|
|
472
|
+
steps.append(step)
|
|
473
|
+
step_idx += 1
|
|
474
|
+
|
|
475
|
+
return Trace(
|
|
476
|
+
problem_id=problem_id,
|
|
477
|
+
steps=steps,
|
|
478
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def _generate_lights_out(self, game: PuzzleGame) -> Trace:
|
|
482
|
+
"""Generate trace for Lights Out puzzle."""
|
|
483
|
+
problem_id = f"lights_out_{game.difficulty.value}_{game.seed}"
|
|
484
|
+
steps: list[Step] = []
|
|
485
|
+
|
|
486
|
+
if hasattr(game, "presses") and game.presses:
|
|
487
|
+
# Lights Out has presses: 2D int grid (1 = press needed)
|
|
488
|
+
presses = game.presses
|
|
489
|
+
step_idx = 0
|
|
490
|
+
size = len(presses)
|
|
491
|
+
|
|
492
|
+
for row in range(size):
|
|
493
|
+
for col in range(len(presses[row])):
|
|
494
|
+
if presses[row][col] == 1:
|
|
495
|
+
step = Step(
|
|
496
|
+
index=step_idx,
|
|
497
|
+
operation=StepOperation.PLACE,
|
|
498
|
+
before_state=f"cell(r{row + 1},c{col + 1})=not_pressed",
|
|
499
|
+
after_state=f"cell(r{row + 1},c{col + 1})=pressed",
|
|
500
|
+
output_value=True,
|
|
501
|
+
position=(row + 1, col + 1),
|
|
502
|
+
rule_applied="xor_toggle",
|
|
503
|
+
explanation=f"Press light at row {row + 1}, column {col + 1} to toggle it and neighbors.",
|
|
504
|
+
)
|
|
505
|
+
steps.append(step)
|
|
506
|
+
step_idx += 1
|
|
507
|
+
|
|
508
|
+
return Trace(
|
|
509
|
+
problem_id=problem_id,
|
|
510
|
+
steps=steps,
|
|
511
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
def _generate_minesweeper(self, game: PuzzleGame) -> Trace:
|
|
515
|
+
"""Generate trace for Minesweeper puzzle."""
|
|
516
|
+
problem_id = f"minesweeper_{game.difficulty.value}_{game.seed}"
|
|
517
|
+
steps: list[Step] = []
|
|
518
|
+
|
|
519
|
+
if hasattr(game, "mines") and game.mines:
|
|
520
|
+
# Minesweeper has mines: 2D bool grid (True = mine)
|
|
521
|
+
mines = game.mines
|
|
522
|
+
size = len(mines)
|
|
523
|
+
step_idx = 0
|
|
524
|
+
|
|
525
|
+
# First, mark all mines as flagged
|
|
526
|
+
for row in range(size):
|
|
527
|
+
for col in range(len(mines[row])):
|
|
528
|
+
if mines[row][col]:
|
|
529
|
+
step = Step(
|
|
530
|
+
index=step_idx,
|
|
531
|
+
operation=StepOperation.ELIMINATE,
|
|
532
|
+
before_state=f"cell(r{row + 1},c{col + 1})=unknown",
|
|
533
|
+
after_state=f"cell(r{row + 1},c{col + 1})=mine",
|
|
534
|
+
output_value=True,
|
|
535
|
+
position=(row + 1, col + 1),
|
|
536
|
+
rule_applied="mine_identification",
|
|
537
|
+
explanation=f"Flag cell at row {row + 1}, column {col + 1} as a mine.",
|
|
538
|
+
)
|
|
539
|
+
steps.append(step)
|
|
540
|
+
step_idx += 1
|
|
541
|
+
|
|
542
|
+
# Then, reveal all safe cells
|
|
543
|
+
for row in range(size):
|
|
544
|
+
for col in range(len(mines[row])):
|
|
545
|
+
if not mines[row][col]:
|
|
546
|
+
counts = getattr(game, "counts", None)
|
|
547
|
+
count = counts[row][col] if counts else 0
|
|
548
|
+
step = Step(
|
|
549
|
+
index=step_idx,
|
|
550
|
+
operation=StepOperation.DEDUCE,
|
|
551
|
+
before_state=f"cell(r{row + 1},c{col + 1})=unknown",
|
|
552
|
+
after_state=f"cell(r{row + 1},c{col + 1})={count}",
|
|
553
|
+
output_value=count,
|
|
554
|
+
position=(row + 1, col + 1),
|
|
555
|
+
rule_applied="safe_reveal",
|
|
556
|
+
explanation=f"Reveal cell at row {row + 1}, column {col + 1} ({count} adjacent mines).",
|
|
557
|
+
)
|
|
558
|
+
steps.append(step)
|
|
559
|
+
step_idx += 1
|
|
560
|
+
|
|
561
|
+
return Trace(
|
|
562
|
+
problem_id=problem_id,
|
|
563
|
+
steps=steps,
|
|
564
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def _generate_scheduler(self, game: PuzzleGame) -> Trace:
|
|
568
|
+
"""Generate trace for Scheduler puzzle."""
|
|
569
|
+
problem_id = f"scheduler_{game.difficulty.value}_{game.seed}"
|
|
570
|
+
steps: list[Step] = []
|
|
571
|
+
|
|
572
|
+
if hasattr(game, "optimal_schedule") and game.optimal_schedule:
|
|
573
|
+
# Scheduler has optimal_schedule: dict[task_id -> (worker_id, start_time)]
|
|
574
|
+
tasks = getattr(game, "tasks", [])
|
|
575
|
+
step_idx = 0
|
|
576
|
+
|
|
577
|
+
# Sort by start time for logical ordering
|
|
578
|
+
sorted_schedule = sorted(game.optimal_schedule.items(), key=lambda x: (x[1][1], x[0]))
|
|
579
|
+
|
|
580
|
+
for task_id, (worker_id, start_time) in sorted_schedule:
|
|
581
|
+
task = tasks[task_id] if task_id < len(tasks) else None
|
|
582
|
+
task_name = task.name if task else f"Task {task_id + 1}"
|
|
583
|
+
duration = task.duration if task else 0
|
|
584
|
+
end_time = start_time + duration
|
|
585
|
+
|
|
586
|
+
step = Step(
|
|
587
|
+
index=step_idx,
|
|
588
|
+
operation=StepOperation.PLACE,
|
|
589
|
+
before_state=f"task({task_name})=unscheduled",
|
|
590
|
+
after_state=f"task({task_name})=worker{worker_id}@{start_time}-{end_time}",
|
|
591
|
+
output_value=(worker_id, start_time),
|
|
592
|
+
position=(task_id + 1,),
|
|
593
|
+
rule_applied="scheduling_constraint",
|
|
594
|
+
explanation=f"Schedule {task_name} on Worker {worker_id} from time {start_time} to {end_time}.",
|
|
595
|
+
)
|
|
596
|
+
steps.append(step)
|
|
597
|
+
step_idx += 1
|
|
598
|
+
|
|
599
|
+
return Trace(
|
|
600
|
+
problem_id=problem_id,
|
|
601
|
+
steps=steps,
|
|
602
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
def _generate_task_scheduler(self, game: PuzzleGame) -> Trace:
|
|
606
|
+
"""Generate trace for Task Scheduler (alias for _generate_scheduler)."""
|
|
607
|
+
return self._generate_scheduler(game)
|
|
608
|
+
|
|
609
|
+
def _generate_slitherlink(self, game: PuzzleGame) -> Trace:
|
|
610
|
+
"""Generate trace for Slitherlink puzzle."""
|
|
611
|
+
problem_id = f"slitherlink_{game.difficulty.value}_{game.seed}"
|
|
612
|
+
steps: list[Step] = []
|
|
613
|
+
|
|
614
|
+
step_idx = 0
|
|
615
|
+
|
|
616
|
+
# Horizontal edges
|
|
617
|
+
if hasattr(game, "solution_h_edges") and game.solution_h_edges:
|
|
618
|
+
for row in range(len(game.solution_h_edges)):
|
|
619
|
+
for col in range(len(game.solution_h_edges[row])):
|
|
620
|
+
if game.solution_h_edges[row][col] == 1:
|
|
621
|
+
step = Step(
|
|
622
|
+
index=step_idx,
|
|
623
|
+
operation=StepOperation.PLACE,
|
|
624
|
+
before_state=f"h_edge(r{row + 1},c{col + 1})=unknown",
|
|
625
|
+
after_state=f"h_edge(r{row + 1},c{col + 1})=line",
|
|
626
|
+
output_value=1,
|
|
627
|
+
position=(row + 1, col + 1),
|
|
628
|
+
rule_applied="loop_constraint",
|
|
629
|
+
explanation=f"Draw horizontal edge at row {row + 1}, column {col + 1}.",
|
|
630
|
+
)
|
|
631
|
+
steps.append(step)
|
|
632
|
+
step_idx += 1
|
|
633
|
+
|
|
634
|
+
# Vertical edges
|
|
635
|
+
if hasattr(game, "solution_v_edges") and game.solution_v_edges:
|
|
636
|
+
for row in range(len(game.solution_v_edges)):
|
|
637
|
+
for col in range(len(game.solution_v_edges[row])):
|
|
638
|
+
if game.solution_v_edges[row][col] == 1:
|
|
639
|
+
step = Step(
|
|
640
|
+
index=step_idx,
|
|
641
|
+
operation=StepOperation.PLACE,
|
|
642
|
+
before_state=f"v_edge(r{row + 1},c{col + 1})=unknown",
|
|
643
|
+
after_state=f"v_edge(r{row + 1},c{col + 1})=line",
|
|
644
|
+
output_value=1,
|
|
645
|
+
position=(row + 1, col + 1),
|
|
646
|
+
rule_applied="loop_constraint",
|
|
647
|
+
explanation=f"Draw vertical edge at row {row + 1}, column {col + 1}.",
|
|
648
|
+
)
|
|
649
|
+
steps.append(step)
|
|
650
|
+
step_idx += 1
|
|
651
|
+
|
|
652
|
+
return Trace(
|
|
653
|
+
problem_id=problem_id,
|
|
654
|
+
steps=steps,
|
|
655
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
def _generate_sokoban(self, game: PuzzleGame) -> Trace:
|
|
659
|
+
"""Generate trace for Sokoban puzzle."""
|
|
660
|
+
problem_id = f"sokoban_{game.difficulty.value}_{game.seed}"
|
|
661
|
+
steps: list[Step] = []
|
|
662
|
+
|
|
663
|
+
if hasattr(game, "goals") and game.goals:
|
|
664
|
+
# For Sokoban, the trace shows the goal state
|
|
665
|
+
# Each box needs to reach a goal position
|
|
666
|
+
step_idx = 0
|
|
667
|
+
|
|
668
|
+
# Find boxes in the initial grid (2 = box, 5 = box on goal)
|
|
669
|
+
grid = getattr(game, "grid", [])
|
|
670
|
+
boxes = []
|
|
671
|
+
for r in range(len(grid)):
|
|
672
|
+
for c in range(len(grid[r])):
|
|
673
|
+
if grid[r][c] in (2, 5):
|
|
674
|
+
boxes.append((r, c))
|
|
675
|
+
|
|
676
|
+
# Match boxes to goals (simplified - assumes 1:1 mapping)
|
|
677
|
+
for i, goal in enumerate(game.goals):
|
|
678
|
+
goal_r, goal_c = goal
|
|
679
|
+
box_pos = boxes[i] if i < len(boxes) else None
|
|
680
|
+
|
|
681
|
+
if box_pos:
|
|
682
|
+
box_r, box_c = box_pos
|
|
683
|
+
step = Step(
|
|
684
|
+
index=step_idx,
|
|
685
|
+
operation=StepOperation.PLACE,
|
|
686
|
+
before_state=f"box{i + 1}=({box_r + 1},{box_c + 1})",
|
|
687
|
+
after_state=f"box{i + 1}=({goal_r + 1},{goal_c + 1})",
|
|
688
|
+
output_value=(goal_r + 1, goal_c + 1),
|
|
689
|
+
position=(goal_r + 1, goal_c + 1),
|
|
690
|
+
rule_applied="goal_placement",
|
|
691
|
+
explanation=f"Push box {i + 1} from ({box_r + 1},{box_c + 1}) to goal at ({goal_r + 1},{goal_c + 1}).",
|
|
692
|
+
)
|
|
693
|
+
steps.append(step)
|
|
694
|
+
step_idx += 1
|
|
695
|
+
|
|
696
|
+
return Trace(
|
|
697
|
+
problem_id=problem_id,
|
|
698
|
+
steps=steps,
|
|
699
|
+
checkpoints=self._identify_checkpoints(steps),
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
def _generate_einstein_s_puzzle(self, game: PuzzleGame) -> Trace:
|
|
703
|
+
"""Generate trace for Einstein's Puzzle (alias with different name format)."""
|
|
704
|
+
return self._generate_einstein(game)
|
|
705
|
+
|
|
706
|
+
def _generate_einstein_puzzle(self, game: PuzzleGame) -> Trace:
|
|
707
|
+
"""Generate trace for Einstein's Puzzle (handles 'einstein's puzzle' -> 'einstein_puzzle')."""
|
|
708
|
+
return self._generate_einstein(game)
|
|
709
|
+
|
|
710
|
+
def _generate_einsteins_puzzle(self, game: PuzzleGame) -> Trace:
|
|
711
|
+
"""Generate trace for Einstein's Puzzle (handles 'einstein's puzzle' -> 'einsteins_puzzle')."""
|
|
712
|
+
return self._generate_einstein(game)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def generate_trace(game: PuzzleGame) -> Trace:
|
|
716
|
+
"""
|
|
717
|
+
Convenience function to generate a trace for a puzzle game.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
game: A puzzle game instance with a generated puzzle
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Trace with step-by-step solution
|
|
724
|
+
"""
|
|
725
|
+
generator = TraceGenerator()
|
|
726
|
+
return generator.generate(game)
|