@fgv/ts-sudoku-lib 5.0.1-9 → 5.0.2-0

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.
Files changed (55) hide show
  1. package/dist/index.browser.js +37 -0
  2. package/dist/index.js +30 -0
  3. package/dist/packlets/collections/collections.js +136 -0
  4. package/dist/packlets/collections/data/puzzles.json +56 -0
  5. package/dist/packlets/collections/index.js +25 -0
  6. package/dist/packlets/common/cage.js +83 -0
  7. package/dist/packlets/common/cell.js +88 -0
  8. package/dist/packlets/common/common.js +70 -0
  9. package/dist/packlets/common/converters.js +65 -0
  10. package/dist/packlets/common/ids.js +152 -0
  11. package/dist/packlets/common/index.js +36 -0
  12. package/dist/packlets/common/logging.js +32 -0
  13. package/dist/packlets/common/public.js +25 -0
  14. package/dist/packlets/common/puzzle.js +411 -0
  15. package/dist/packlets/common/puzzleDefinitions.js +247 -0
  16. package/dist/packlets/common/puzzleSession.js +369 -0
  17. package/dist/packlets/common/puzzleState.js +99 -0
  18. package/dist/packlets/files/converters.js +67 -0
  19. package/dist/packlets/files/fileTreeHelpers.js +37 -0
  20. package/dist/packlets/files/filesystem.js +37 -0
  21. package/dist/packlets/files/index.browser.js +32 -0
  22. package/dist/packlets/files/index.js +30 -0
  23. package/dist/packlets/files/model.js +25 -0
  24. package/dist/packlets/hints/baseHintProvider.js +199 -0
  25. package/dist/packlets/hints/explanations.js +270 -0
  26. package/dist/packlets/hints/hiddenSingles.js +233 -0
  27. package/dist/packlets/hints/hintRegistry.js +222 -0
  28. package/dist/packlets/hints/hints.js +311 -0
  29. package/dist/packlets/hints/index.js +39 -0
  30. package/dist/packlets/hints/interfaces.js +25 -0
  31. package/dist/packlets/hints/nakedSingles.js +198 -0
  32. package/dist/packlets/hints/puzzleSessionHints.js +495 -0
  33. package/dist/packlets/hints/types.js +43 -0
  34. package/dist/packlets/puzzles/anyPuzzle.js +47 -0
  35. package/dist/packlets/puzzles/index.js +31 -0
  36. package/dist/packlets/puzzles/internal/combinationCache.js +75 -0
  37. package/dist/packlets/puzzles/internal/combinationGenerator.js +142 -0
  38. package/dist/packlets/puzzles/internal/possibilityAnalyzer.js +121 -0
  39. package/dist/packlets/puzzles/killerCombinations.js +222 -0
  40. package/dist/packlets/puzzles/killerCombinationsTypes.js +25 -0
  41. package/dist/packlets/puzzles/killerSudokuPuzzle.js +136 -0
  42. package/dist/packlets/puzzles/sudokuPuzzle.js +43 -0
  43. package/dist/packlets/puzzles/sudokuXPuzzle.js +65 -0
  44. package/dist/test/unit/helpers/puzzleBuilders.js +149 -0
  45. package/dist/ts-sudoku-lib.d.ts +12 -7
  46. package/dist/tsdoc-metadata.json +1 -1
  47. package/lib/index.browser.d.ts +7 -0
  48. package/lib/index.browser.js +78 -0
  49. package/lib/packlets/files/fileTreeHelpers.d.ts +13 -0
  50. package/lib/packlets/files/fileTreeHelpers.js +40 -0
  51. package/lib/packlets/files/index.browser.d.ts +5 -0
  52. package/lib/packlets/files/index.browser.js +70 -0
  53. package/lib/packlets/files/index.d.ts +3 -2
  54. package/lib/packlets/files/index.js +7 -4
  55. package/package.json +23 -5
@@ -0,0 +1,198 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2023 Erik Fortune
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ import { succeed } from '@fgv/ts-utils';
25
+ import { Ids, parseCellId } from '../common';
26
+ import { BaseHintProvider } from './baseHintProvider';
27
+ import { ConfidenceLevels, TechniqueIds } from './types';
28
+ /**
29
+ * Hint provider for the Naked Singles technique.
30
+ *
31
+ * A Naked Single occurs when a cell has only one possible candidate value
32
+ * based on the constraints of its row, column, and 3x3 box.
33
+ *
34
+ * @public
35
+ */
36
+ export class NakedSinglesProvider extends BaseHintProvider {
37
+ /**
38
+ * Creates a new NakedSinglesProvider instance.
39
+ */
40
+ constructor() {
41
+ super({
42
+ techniqueId: TechniqueIds.NAKED_SINGLES,
43
+ techniqueName: 'Naked Singles',
44
+ difficulty: 'beginner',
45
+ priority: 1, // Highest priority - should be checked first
46
+ defaultConfidence: ConfidenceLevels.HIGH
47
+ });
48
+ }
49
+ /**
50
+ * Determines if this provider can potentially generate hints for the given puzzle.
51
+ * Always returns true since naked singles can potentially exist in any incomplete puzzle.
52
+ * @param puzzle - The puzzle structure containing constraints
53
+ * @param state - The current puzzle state
54
+ * @returns true if there are empty cells that might have naked singles
55
+ */
56
+ canProvideHints(puzzle, state) {
57
+ // Quick check: if there are empty cells, there might be naked singles
58
+ const emptyCells = this.getEmptyCells(puzzle, state);
59
+ return emptyCells.length > 0;
60
+ }
61
+ /**
62
+ * Generates all naked single hints for the given puzzle.
63
+ * @param puzzle - The puzzle structure containing constraints
64
+ * @param state - The current puzzle state
65
+ * @param options - Optional generation options
66
+ * @returns Result containing array of naked single hints
67
+ */
68
+ generateHints(puzzle, state, options) {
69
+ return this.validateOptions(options).onSuccess(() => {
70
+ const hints = [];
71
+ const emptyCells = this.getEmptyCells(puzzle, state);
72
+ for (const cellId of emptyCells) {
73
+ const candidates = this.getCandidates(cellId, puzzle, state);
74
+ // A naked single has exactly one candidate
75
+ if (candidates.length === 1) {
76
+ const value = candidates[0];
77
+ const hint = this._createNakedSingleHint(cellId, value, state, puzzle);
78
+ hints.push(hint);
79
+ }
80
+ }
81
+ return succeed(this.filterHints(hints, options));
82
+ });
83
+ }
84
+ /**
85
+ * Creates a hint for a specific naked single.
86
+ * @param cellId - The cell containing the naked single
87
+ * @param value - The single possible value for the cell
88
+ * @param state - The current puzzle state
89
+ * @returns A complete hint for this naked single
90
+ */
91
+ _createNakedSingleHint(cellId, value, state, puzzle) {
92
+ // Create cell action to set the value
93
+ const cellAction = this.createCellAction(cellId, 'set-value', value, `Only possible value for this cell`);
94
+ // Find related cells that eliminate other candidates
95
+ const relatedCells = this._findRelatedCells(cellId, value, state, puzzle);
96
+ // Create relevant cells grouping
97
+ const relevantCells = this.createRelevantCells([cellId], // Primary: the cell with the naked single
98
+ relatedCells, // Secondary: cells that constrain this cell
99
+ [] // Affected: none for naked singles
100
+ );
101
+ // Create explanations at different levels
102
+ const explanations = [
103
+ this.createExplanation('brief', 'Naked Single', `Cell ${cellId} can only contain the value ${value}.`, [`Set ${cellId} = ${value}`], ['Look for cells with only one possible value']),
104
+ this.createExplanation('detailed', 'Naked Single Analysis', `Cell ${cellId} has only one possible candidate: ${value}. All other values (1-9) are ` +
105
+ `eliminated by existing numbers in the same row, column, or 3x3 box.`, [
106
+ `Examine cell ${cellId}`,
107
+ `Check which values 1-9 are already used in the same row, column, and 3x3 box`,
108
+ `Identify that only ${value} is not eliminated`,
109
+ `Set ${cellId} = ${value}`
110
+ ], [
111
+ 'Naked singles are the most basic solving technique',
112
+ 'Always check for naked singles first',
113
+ 'A cell with one candidate must contain that value'
114
+ ]),
115
+ this.createExplanation('educational', 'Understanding Naked Singles', `A naked single occurs when a cell has only one possible value due to Sudoku's fundamental rules. ` +
116
+ `In cell ${cellId}, the value ${value} is the only number from 1-${puzzle.dimensions.maxValue} that doesn't already appear in the ` +
117
+ `same row, column, or section. This makes it a "naked" single because the solution is immediately visible.`, [
118
+ `Locate cell ${cellId} in the grid`,
119
+ `Scan the row containing ${cellId} and note which numbers 1-${puzzle.dimensions.maxValue} are already placed`,
120
+ `Scan the column containing ${cellId} and note which numbers 1-${puzzle.dimensions.maxValue} are already placed`,
121
+ `Scan the section containing ${cellId} and note which numbers 1-${puzzle.dimensions.maxValue} are already placed`,
122
+ `Combine all the "used" numbers from row, column, and section`,
123
+ `The remaining unused number is ${value}, which must go in ${cellId}`,
124
+ `Place ${value} in cell ${cellId}`
125
+ ], [
126
+ 'Naked singles are always correct - there is no risk in placing them',
127
+ 'Start every solving session by finding all naked singles',
128
+ 'Placing naked singles often reveals new naked singles in related cells',
129
+ 'This technique forms the foundation for all other Sudoku solving methods'
130
+ ])
131
+ ];
132
+ return this.createHint([cellAction], relevantCells, explanations, ConfidenceLevels.HIGH);
133
+ }
134
+ /**
135
+ * Finds cells in the same row, column, and box that constrain the given cell.
136
+ * @param cellId - The cell to analyze
137
+ * @param value - The value that will be placed
138
+ * @param state - The current puzzle state
139
+ * @param puzzle - The puzzle for dimensions
140
+ * @returns Array of cell IDs that are related to this naked single
141
+ */
142
+ _findRelatedCells(cellId, value, state, puzzle) {
143
+ const relatedCells = [];
144
+ // Parse the cell ID to get row and column
145
+ const coords = parseCellId(cellId.toString());
146
+ /* c8 ignore next 3 - defensive coding: handles corrupted cell ID, impractical to test via public API */
147
+ if (!coords) {
148
+ return relatedCells;
149
+ }
150
+ const { row, col } = coords;
151
+ const totalRows = puzzle.dimensions.totalRows;
152
+ const totalColumns = puzzle.dimensions.totalColumns;
153
+ const cageHeight = puzzle.dimensions.cageHeightInCells;
154
+ const cageWidth = puzzle.dimensions.cageWidthInCells;
155
+ // Find cells in the same row, column, and section that contain any value
156
+ // These are the cells that constrain our naked single
157
+ // Same row
158
+ for (let c = 0; c < totalColumns; c++) {
159
+ if (c !== col) {
160
+ const checkIdResult = Ids.cellId({ row, col: c });
161
+ if (checkIdResult.isSuccess() && state.hasValue(checkIdResult.value)) {
162
+ relatedCells.push(checkIdResult.value);
163
+ }
164
+ }
165
+ }
166
+ // Same column
167
+ for (let r = 0; r < totalRows; r++) {
168
+ if (r !== row) {
169
+ const checkIdResult = Ids.cellId({ row: r, col });
170
+ if (checkIdResult.isSuccess() && state.hasValue(checkIdResult.value)) {
171
+ relatedCells.push(checkIdResult.value);
172
+ }
173
+ }
174
+ }
175
+ // Same section/cage
176
+ const boxStartRow = Math.floor(row / cageHeight) * cageHeight;
177
+ const boxStartCol = Math.floor(col / cageWidth) * cageWidth;
178
+ for (let r = boxStartRow; r < boxStartRow + cageHeight; r++) {
179
+ for (let c = boxStartCol; c < boxStartCol + cageWidth; c++) {
180
+ if (r !== row || c !== col) {
181
+ const checkIdResult = Ids.cellId({ row: r, col: c });
182
+ if (checkIdResult.isSuccess() && state.hasValue(checkIdResult.value)) {
183
+ relatedCells.push(checkIdResult.value);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ return relatedCells;
189
+ }
190
+ /**
191
+ * Static factory method to create a new NakedSinglesProvider.
192
+ * @returns Result containing the new provider
193
+ */
194
+ static create() {
195
+ return succeed(new NakedSinglesProvider());
196
+ }
197
+ }
198
+ //# sourceMappingURL=nakedSingles.js.map
@@ -0,0 +1,495 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2023 Erik Fortune
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ import { succeed } from '@fgv/ts-utils';
25
+ import { Ids } from '../common';
26
+ import { HintSystem } from './hints';
27
+ /**
28
+ * Wrapper class that integrates hint functionality with PuzzleSession.
29
+ * Provides hint generation, application, and explanation capabilities while
30
+ * maintaining integration with existing state management and undo/redo functionality.
31
+ * @public
32
+ */
33
+ export class PuzzleSessionHints {
34
+ /**
35
+ * @internal
36
+ */
37
+ constructor(session, hintSystem, config) {
38
+ this._session = session;
39
+ this._hintSystem = hintSystem;
40
+ this._config = config;
41
+ this._hintCache = undefined;
42
+ }
43
+ /**
44
+ * Creates a new PuzzleSessionHints wrapper for an existing PuzzleSession.
45
+ * @param session - The PuzzleSession to wrap
46
+ * @param config - Optional configuration for the hint system
47
+ * @returns Result containing the new PuzzleSessionHints wrapper
48
+ */
49
+ static create(session, config) {
50
+ const finalConfig = Object.assign({ enableNakedSingles: true, enableHiddenSingles: true, defaultExplanationLevel: 'detailed', cacheTimeoutMs: 5000, maxCacheEntries: 1 }, config);
51
+ return HintSystem.create(finalConfig).onSuccess((hintSystem) => {
52
+ return succeed(new PuzzleSessionHints(session, hintSystem, finalConfig));
53
+ });
54
+ }
55
+ /**
56
+ * Gets the wrapped PuzzleSession instance.
57
+ * @returns The underlying PuzzleSession
58
+ */
59
+ get session() {
60
+ return this._session;
61
+ }
62
+ /**
63
+ * Gets the HintSystem instance.
64
+ * @returns The hint system
65
+ */
66
+ get hintSystem() {
67
+ return this._hintSystem;
68
+ }
69
+ /**
70
+ * Gets the configuration.
71
+ * @returns The configuration
72
+ */
73
+ get config() {
74
+ return this._config;
75
+ }
76
+ // Delegate all PuzzleSession methods for transparent access
77
+ /**
78
+ * Gets the puzzle ID.
79
+ * @returns The puzzle ID
80
+ */
81
+ get id() {
82
+ return this._session.id;
83
+ }
84
+ /**
85
+ * Gets the puzzle description.
86
+ * @returns The puzzle description
87
+ */
88
+ get description() {
89
+ return this._session.description;
90
+ }
91
+ /**
92
+ * Gets the number of rows in the puzzle.
93
+ * @returns The number of rows
94
+ */
95
+ get numRows() {
96
+ return this._session.numRows;
97
+ }
98
+ /**
99
+ * Gets the number of columns in the puzzle.
100
+ * @returns The number of columns
101
+ */
102
+ get numColumns() {
103
+ return this._session.numColumns;
104
+ }
105
+ /**
106
+ * Gets the current puzzle state.
107
+ * @returns The current state
108
+ */
109
+ get state() {
110
+ return this._session.state;
111
+ }
112
+ /**
113
+ * Gets whether undo is possible.
114
+ * @returns true if undo is possible
115
+ */
116
+ get canUndo() {
117
+ return this._session.canUndo;
118
+ }
119
+ /**
120
+ * Gets whether redo is possible.
121
+ * @returns true if redo is possible
122
+ */
123
+ get canRedo() {
124
+ return this._session.canRedo;
125
+ }
126
+ /**
127
+ * Checks if the puzzle is solved.
128
+ * @returns true if the puzzle is solved
129
+ */
130
+ checkIsSolved() {
131
+ return this._session.checkIsSolved();
132
+ }
133
+ /**
134
+ * Checks if the puzzle is valid.
135
+ * @returns true if the puzzle is valid
136
+ */
137
+ checkIsValid() {
138
+ return this._session.checkIsValid();
139
+ }
140
+ /**
141
+ * Updates a cell value.
142
+ * @param spec - Cell specification
143
+ * @param value - New value
144
+ * @returns Result with this instance
145
+ */
146
+ updateCellValue(spec, value) {
147
+ return this._session.updateCellValue(spec, value).onSuccess(() => {
148
+ this._invalidateCache();
149
+ return succeed(this);
150
+ });
151
+ }
152
+ /**
153
+ * Updates cell notes.
154
+ * @param spec - Cell specification
155
+ * @param notes - New notes
156
+ * @returns Result with this instance
157
+ */
158
+ updateCellNotes(spec, notes) {
159
+ return this._session.updateCellNotes(spec, notes).onSuccess(() => {
160
+ this._invalidateCache();
161
+ return succeed(this);
162
+ });
163
+ }
164
+ /**
165
+ * Updates multiple cells.
166
+ * @param updates - Array of cell updates
167
+ * @returns Result with this instance
168
+ */
169
+ updateCells(updates) {
170
+ return this._session.updateCells(updates).onSuccess(() => {
171
+ this._invalidateCache();
172
+ return succeed(this);
173
+ });
174
+ }
175
+ /**
176
+ * Performs an undo operation.
177
+ * @returns Result with this instance
178
+ */
179
+ undo() {
180
+ return this._session.undo().onSuccess(() => {
181
+ this._invalidateCache();
182
+ return succeed(this);
183
+ });
184
+ }
185
+ /**
186
+ * Performs a redo operation.
187
+ * @returns Result with this instance
188
+ */
189
+ redo() {
190
+ return this._session.redo().onSuccess(() => {
191
+ this._invalidateCache();
192
+ return succeed(this);
193
+ });
194
+ }
195
+ // Hint-specific functionality
196
+ /**
197
+ * Gets the best available hint for the current puzzle state.
198
+ * @param options - Optional hint generation options
199
+ * @returns Result containing the best hint
200
+ */
201
+ getHint(options) {
202
+ return this._hintSystem.getBestHint(this._session.puzzle, this._session.state, options);
203
+ }
204
+ /**
205
+ * Gets all available hints for the current puzzle state.
206
+ * @param options - Optional hint generation options
207
+ * @returns Result containing array of hints
208
+ */
209
+ getAllHints(options) {
210
+ // Check cache first
211
+ const cachedHints = this._getCachedHints(options);
212
+ if (cachedHints) {
213
+ return succeed(cachedHints);
214
+ }
215
+ // Generate new hints
216
+ return this._hintSystem
217
+ .generateHints(this._session.puzzle, this._session.state, options)
218
+ .onSuccess((hints) => {
219
+ this._updateCache(hints, options);
220
+ return succeed(hints);
221
+ });
222
+ }
223
+ /**
224
+ * Applies a hint to the puzzle, updating the state and adding to undo history.
225
+ * @param hint - The hint to apply
226
+ * @returns Result with this instance if successful
227
+ */
228
+ applyHint(hint) {
229
+ return this._hintSystem
230
+ .applyHint(hint, this._session.puzzle, this._session.state)
231
+ .onSuccess((updates) => {
232
+ // Convert readonly array to mutable array for PuzzleSession.updateCells
233
+ const mutableUpdates = [...updates];
234
+ return this._session.updateCells(mutableUpdates).onSuccess(() => {
235
+ this._invalidateCache();
236
+ return succeed(this);
237
+ });
238
+ });
239
+ }
240
+ /**
241
+ * Gets hints that specifically affect a given cell.
242
+ * @param spec - Cell specification (ID, row/column, or cell object)
243
+ * @param options - Optional hint generation options
244
+ * @returns Result containing hints affecting the specified cell
245
+ */
246
+ getHintsForCell(spec, options) {
247
+ const cellIdResult = Ids.cellId(spec);
248
+ return cellIdResult.onSuccess((cellId) => {
249
+ return this.getAllHints(options).onSuccess((allHints) => {
250
+ const relevantHints = allHints.filter((hint) => {
251
+ // Check if the hint affects this specific cell
252
+ return (hint.cellActions.some((action) => action.cellId === cellId) ||
253
+ hint.relevantCells.primary.includes(cellId) ||
254
+ hint.relevantCells.secondary.includes(cellId) ||
255
+ hint.relevantCells.affected.includes(cellId));
256
+ });
257
+ return succeed(relevantHints);
258
+ });
259
+ });
260
+ }
261
+ /**
262
+ * Gets a formatted explanation for a hint.
263
+ * @param hint - The hint to explain
264
+ * @param level - The explanation level (defaults to configured default)
265
+ * @returns Formatted explanation string
266
+ */
267
+ getExplanation(hint, level) {
268
+ return this._hintSystem.formatHintExplanation(hint, level);
269
+ }
270
+ /**
271
+ * Validates that a hint can be applied to the current state.
272
+ * @param hint - The hint to validate
273
+ * @returns Result indicating validation success or failure
274
+ */
275
+ validateHint(hint) {
276
+ return this._hintSystem.validateHint(hint, this._session.puzzle, this._session.state);
277
+ }
278
+ /**
279
+ * Checks if hints are available for the current state.
280
+ * @param options - Optional hint generation options
281
+ * @returns Result containing boolean indicating availability
282
+ */
283
+ hasHints(options) {
284
+ return this._hintSystem.hasHints(this._session.puzzle, this._session.state);
285
+ }
286
+ /**
287
+ * Gets statistics about available hints.
288
+ * @param options - Optional hint generation options
289
+ * @returns Result containing hint statistics
290
+ */
291
+ getHintStatistics(options) {
292
+ return this._hintSystem.getHintStatistics(this._session.puzzle, this._session.state);
293
+ }
294
+ /**
295
+ * Gets a summary of the hint system capabilities.
296
+ * @returns System capabilities summary
297
+ */
298
+ getSystemSummary() {
299
+ return this._hintSystem.getSystemSummary();
300
+ }
301
+ // Delegate remaining PuzzleSession methods for complete transparency
302
+ /**
303
+ * Gets the rows.
304
+ * @returns Array of row cages
305
+ */
306
+ get rows() {
307
+ return this._session.rows;
308
+ }
309
+ /**
310
+ * Gets the columns.
311
+ * @returns Array of column cages
312
+ */
313
+ get cols() {
314
+ return this._session.cols;
315
+ }
316
+ /**
317
+ * Gets the sections.
318
+ * @returns Array of section cages
319
+ */
320
+ get sections() {
321
+ return this._session.sections;
322
+ }
323
+ /**
324
+ * Gets all cages.
325
+ * @returns Array of all cages
326
+ */
327
+ get cages() {
328
+ return this._session.cages;
329
+ }
330
+ /**
331
+ * Gets all cells.
332
+ * @returns Array of all cells
333
+ */
334
+ get cells() {
335
+ return this._session.cells;
336
+ }
337
+ /**
338
+ * Gets the next step index.
339
+ * @returns Next step index
340
+ */
341
+ get nextStep() {
342
+ return this._session.nextStep;
343
+ }
344
+ /**
345
+ * Gets the number of steps.
346
+ * @returns Number of steps
347
+ */
348
+ get numSteps() {
349
+ return this._session.numSteps;
350
+ }
351
+ /**
352
+ * Gets empty cells.
353
+ * @returns Array of empty cells
354
+ */
355
+ getEmptyCells() {
356
+ return this._session.getEmptyCells();
357
+ }
358
+ /**
359
+ * Gets invalid cells.
360
+ * @returns Array of invalid cells
361
+ */
362
+ getInvalidCells() {
363
+ return this._session.getInvalidCells();
364
+ }
365
+ /**
366
+ * Checks if a cell is valid.
367
+ * @param spec - Cell specification
368
+ * @returns true if valid
369
+ */
370
+ cellIsValid(spec) {
371
+ return this._session.cellIsValid(spec);
372
+ }
373
+ /**
374
+ * Checks if a cell has a value.
375
+ * @param spec - Cell specification
376
+ * @returns true if cell has value
377
+ */
378
+ cellHasValue(spec) {
379
+ return this._session.cellHasValue(spec);
380
+ }
381
+ /**
382
+ * Checks if a value is valid for a cell.
383
+ * @param spec - Cell specification
384
+ * @param value - Value to check
385
+ * @returns true if valid
386
+ */
387
+ isValidForCell(spec, value) {
388
+ return this._session.isValidForCell(spec, value);
389
+ }
390
+ /**
391
+ * Gets a cell neighbor.
392
+ * @param spec - Cell specification
393
+ * @param direction - Navigation direction
394
+ * @param wrap - Wrap behavior
395
+ * @returns Result containing neighbor cell
396
+ */
397
+ getCellNeighbor(spec, direction, wrap) {
398
+ return this._session.getCellNeighbor(spec, direction, wrap);
399
+ }
400
+ /**
401
+ * Gets cell contents.
402
+ * @param spec - Cell specification
403
+ * @returns Result containing cell and contents
404
+ */
405
+ getCellContents(spec) {
406
+ return this._session.getCellContents(spec);
407
+ }
408
+ /**
409
+ * Checks if a cage contains a value.
410
+ * @param spec - Cage specification
411
+ * @param value - Value to check
412
+ * @returns true if cage contains value
413
+ */
414
+ cageContainsValue(spec, value) {
415
+ return this._session.cageContainsValue(spec, value);
416
+ }
417
+ /**
418
+ * Gets contained values in a cage.
419
+ * @param spec - Cage specification
420
+ * @returns Set of contained values
421
+ */
422
+ cageContainedValues(spec) {
423
+ return this._session.cageContainedValues(spec);
424
+ }
425
+ /**
426
+ * Gets string representation of the puzzle.
427
+ * @returns Array of strings representing puzzle rows
428
+ */
429
+ toStrings() {
430
+ return this._session.toStrings();
431
+ }
432
+ // Private cache management methods
433
+ /**
434
+ * Generates a cache key for the current state and options.
435
+ * @param options - Hint generation options
436
+ * @returns Cache key
437
+ */
438
+ _generateCacheKey(options) {
439
+ // Simple state hash based on cell values and notes
440
+ const stateString = this._session.toStrings().join('|');
441
+ const notesString = this._session.cells
442
+ .map((cell) => {
443
+ const contents = this._session
444
+ .getCellContents(cell.id)
445
+ .orDefault({ cell, contents: { value: undefined, notes: [] } });
446
+ return `${cell.id}:${contents.contents.notes.join(',')}`;
447
+ })
448
+ .join('|');
449
+ const stateHash = `${stateString}#${notesString}`;
450
+ const optionsString = options ? JSON.stringify(options) : '';
451
+ return { stateHash, options: optionsString };
452
+ }
453
+ /**
454
+ * Gets cached hints if valid.
455
+ * @param options - Hint generation options
456
+ * @returns Cached hints or undefined
457
+ */
458
+ _getCachedHints(options) {
459
+ var _a;
460
+ if (!this._hintCache) {
461
+ return undefined;
462
+ }
463
+ const cacheKey = this._generateCacheKey(options);
464
+ /* c8 ignore next 1 - ?? is defense in depth */
465
+ const timeoutMs = (_a = this._config.cacheTimeoutMs) !== null && _a !== void 0 ? _a : 5000;
466
+ const now = Date.now();
467
+ // Check if cache is still valid
468
+ if (this._hintCache.cacheKey.stateHash === cacheKey.stateHash &&
469
+ this._hintCache.cacheKey.options === cacheKey.options &&
470
+ now - this._hintCache.generatedAt < timeoutMs) {
471
+ return this._hintCache.hints;
472
+ }
473
+ return undefined;
474
+ }
475
+ /**
476
+ * Updates the cache with new hints.
477
+ * @param hints - Generated hints
478
+ * @param options - Generation options
479
+ */
480
+ _updateCache(hints, options) {
481
+ const cacheKey = this._generateCacheKey(options);
482
+ this._hintCache = {
483
+ hints,
484
+ generatedAt: Date.now(),
485
+ cacheKey
486
+ };
487
+ }
488
+ /**
489
+ * Invalidates the current hint cache.
490
+ */
491
+ _invalidateCache() {
492
+ this._hintCache = undefined;
493
+ }
494
+ }
495
+ //# sourceMappingURL=puzzleSessionHints.js.map