@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.
- package/dist/index.browser.js +37 -0
- package/dist/index.js +30 -0
- package/dist/packlets/collections/collections.js +136 -0
- package/dist/packlets/collections/data/puzzles.json +56 -0
- package/dist/packlets/collections/index.js +25 -0
- package/dist/packlets/common/cage.js +83 -0
- package/dist/packlets/common/cell.js +88 -0
- package/dist/packlets/common/common.js +70 -0
- package/dist/packlets/common/converters.js +65 -0
- package/dist/packlets/common/ids.js +152 -0
- package/dist/packlets/common/index.js +36 -0
- package/dist/packlets/common/logging.js +32 -0
- package/dist/packlets/common/public.js +25 -0
- package/dist/packlets/common/puzzle.js +411 -0
- package/dist/packlets/common/puzzleDefinitions.js +247 -0
- package/dist/packlets/common/puzzleSession.js +369 -0
- package/dist/packlets/common/puzzleState.js +99 -0
- package/dist/packlets/files/converters.js +67 -0
- package/dist/packlets/files/fileTreeHelpers.js +37 -0
- package/dist/packlets/files/filesystem.js +37 -0
- package/dist/packlets/files/index.browser.js +32 -0
- package/dist/packlets/files/index.js +30 -0
- package/dist/packlets/files/model.js +25 -0
- package/dist/packlets/hints/baseHintProvider.js +199 -0
- package/dist/packlets/hints/explanations.js +270 -0
- package/dist/packlets/hints/hiddenSingles.js +233 -0
- package/dist/packlets/hints/hintRegistry.js +222 -0
- package/dist/packlets/hints/hints.js +311 -0
- package/dist/packlets/hints/index.js +39 -0
- package/dist/packlets/hints/interfaces.js +25 -0
- package/dist/packlets/hints/nakedSingles.js +198 -0
- package/dist/packlets/hints/puzzleSessionHints.js +495 -0
- package/dist/packlets/hints/types.js +43 -0
- package/dist/packlets/puzzles/anyPuzzle.js +47 -0
- package/dist/packlets/puzzles/index.js +31 -0
- package/dist/packlets/puzzles/internal/combinationCache.js +75 -0
- package/dist/packlets/puzzles/internal/combinationGenerator.js +142 -0
- package/dist/packlets/puzzles/internal/possibilityAnalyzer.js +121 -0
- package/dist/packlets/puzzles/killerCombinations.js +222 -0
- package/dist/packlets/puzzles/killerCombinationsTypes.js +25 -0
- package/dist/packlets/puzzles/killerSudokuPuzzle.js +136 -0
- package/dist/packlets/puzzles/sudokuPuzzle.js +43 -0
- package/dist/packlets/puzzles/sudokuXPuzzle.js +65 -0
- package/dist/test/unit/helpers/puzzleBuilders.js +149 -0
- package/dist/ts-sudoku-lib.d.ts +12 -7
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/index.browser.d.ts +7 -0
- package/lib/index.browser.js +78 -0
- package/lib/packlets/files/fileTreeHelpers.d.ts +13 -0
- package/lib/packlets/files/fileTreeHelpers.js +40 -0
- package/lib/packlets/files/index.browser.d.ts +5 -0
- package/lib/packlets/files/index.browser.js +70 -0
- package/lib/packlets/files/index.d.ts +3 -2
- package/lib/packlets/files/index.js +7 -4
- 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
|