@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,270 @@
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 { fail, succeed } from '@fgv/ts-utils';
25
+ import { TechniqueIds } from './types';
26
+ /**
27
+ * Registry for managing hint explanation providers.
28
+ * @public
29
+ */
30
+ export class ExplanationRegistry {
31
+ /**
32
+ * Creates a new ExplanationRegistry instance.
33
+ */
34
+ constructor() {
35
+ this._providers = new Map();
36
+ }
37
+ /**
38
+ * Registers a new explanation provider.
39
+ * @param provider - The provider to register
40
+ * @returns Result indicating success or failure of registration
41
+ */
42
+ registerProvider(provider) {
43
+ if (this._providers.has(provider.techniqueId)) {
44
+ return fail(`Explanation provider for technique ${provider.techniqueId} is already registered`);
45
+ }
46
+ this._providers.set(provider.techniqueId, provider);
47
+ return succeed(undefined);
48
+ }
49
+ /**
50
+ * Gets explanations for a specific hint.
51
+ * @param hint - The hint to explain
52
+ * @param puzzle - The puzzle structure containing constraints
53
+ * @param state - The puzzle state context
54
+ * @returns Result containing the explanations
55
+ */
56
+ getExplanations(hint, puzzle, state) {
57
+ const provider = this._providers.get(hint.techniqueId);
58
+ if (!provider) {
59
+ return fail(`No explanation provider found for technique ${hint.techniqueId}`);
60
+ }
61
+ return provider.generateExplanations(hint, puzzle, state);
62
+ }
63
+ /**
64
+ * Gets a specific explanation at the requested level.
65
+ * @param hint - The hint to explain
66
+ * @param level - The desired explanation level
67
+ * @param state - The puzzle state context
68
+ * @returns Result containing the explanation at the specified level
69
+ */
70
+ getExplanationAtLevel(hint, level, puzzle, state) {
71
+ return this.getExplanations(hint, puzzle, state).onSuccess((explanations) => {
72
+ const explanation = explanations.find((exp) => exp.level === level);
73
+ return explanation
74
+ ? succeed(explanation)
75
+ : fail(`No explanation available at level ${level} for technique ${hint.techniqueId}`);
76
+ });
77
+ }
78
+ }
79
+ /**
80
+ * Utility class for formatting and displaying hint explanations.
81
+ * @public
82
+ */
83
+ export class ExplanationFormatter {
84
+ /**
85
+ * Formats a hint explanation as a readable string.
86
+ * @param explanation - The explanation to format
87
+ * @param includeSteps - Whether to include step-by-step instructions
88
+ * @param includeTips - Whether to include tips
89
+ * @returns Formatted explanation string
90
+ */
91
+ static formatExplanation(explanation, includeSteps = true, includeTips = true) {
92
+ const sections = [];
93
+ // Title and description
94
+ sections.push(`${explanation.title}`);
95
+ sections.push(`${explanation.description}`);
96
+ // Steps
97
+ if (includeSteps && explanation.steps && explanation.steps.length > 0) {
98
+ sections.push('\nSteps:');
99
+ explanation.steps.forEach((step, index) => {
100
+ sections.push(`${index + 1}. ${step}`);
101
+ });
102
+ }
103
+ // Tips
104
+ if (includeTips && explanation.tips && explanation.tips.length > 0) {
105
+ sections.push('\nTips:');
106
+ explanation.tips.forEach((tip) => {
107
+ sections.push(`• ${tip}`);
108
+ });
109
+ }
110
+ return sections.join('\n');
111
+ }
112
+ /**
113
+ * Formats all explanations for a hint as a structured string.
114
+ * @param explanations - The explanations to format
115
+ * @returns Formatted explanations string
116
+ */
117
+ static formatAllExplanations(explanations) {
118
+ const sections = [];
119
+ for (const explanation of explanations) {
120
+ sections.push(`--- ${explanation.level.toUpperCase()} ---`);
121
+ sections.push(this.formatExplanation(explanation));
122
+ sections.push('');
123
+ }
124
+ return sections.join('\n');
125
+ }
126
+ /**
127
+ * Creates a summary of available explanation levels.
128
+ * @param explanations - The explanations to summarize
129
+ * @returns Summary string
130
+ */
131
+ static createLevelSummary(explanations) {
132
+ const levels = explanations.map((exp) => exp.level).join(', ');
133
+ return `Available explanation levels: ${levels}`;
134
+ }
135
+ }
136
+ /**
137
+ * Educational content manager for Sudoku solving techniques.
138
+ * @public
139
+ */
140
+ export class EducationalContent {
141
+ /**
142
+ * Gets an introduction to a specific technique.
143
+ * @param techniqueId - The technique to introduce
144
+ * @returns Result containing the introduction text
145
+ */
146
+ static getTechniqueIntroduction(techniqueId) {
147
+ const introduction = this._techniqueIntroductions.get(techniqueId);
148
+ return introduction
149
+ ? succeed(introduction)
150
+ : fail(`No introduction available for technique ${techniqueId}`);
151
+ }
152
+ /**
153
+ * Gets relationship information for a technique.
154
+ * @param techniqueId - The technique to get relationships for
155
+ * @returns Result containing the relationship information
156
+ */
157
+ static getTechniqueRelationships(techniqueId) {
158
+ const relationships = this._techniqueRelationships.get(techniqueId);
159
+ return relationships
160
+ ? succeed(relationships)
161
+ : fail(`No relationship information available for technique ${techniqueId}`);
162
+ }
163
+ /**
164
+ * Gets a complete educational overview for a technique.
165
+ * @param techniqueId - The technique to describe
166
+ * @returns Result containing the complete overview
167
+ */
168
+ static getTechniqueOverview(techniqueId) {
169
+ const introduction = this.getTechniqueIntroduction(techniqueId);
170
+ const relationships = this.getTechniqueRelationships(techniqueId);
171
+ return introduction.onSuccess((intro) => {
172
+ return relationships.onSuccess((rels) => {
173
+ const sections = [intro, '', 'Technique Relationships:', ...rels.map((rel) => `• ${rel}`)];
174
+ return succeed(sections.join('\n'));
175
+ });
176
+ });
177
+ }
178
+ /**
179
+ * Gets general Sudoku solving advice.
180
+ * @returns Array of general solving tips
181
+ */
182
+ static getGeneralSolvingTips() {
183
+ return [
184
+ 'Start with naked singles - they are always correct when found',
185
+ 'After placing numbers, scan for new naked singles immediately',
186
+ 'Check for hidden singles systematically in rows, columns, and boxes',
187
+ 'Focus on units with many filled cells first',
188
+ 'Keep track of candidates mentally or with pencil marks',
189
+ 'Take breaks when stuck - fresh eyes often spot overlooked patterns',
190
+ 'Practice pattern recognition to spot techniques more quickly'
191
+ ];
192
+ }
193
+ /**
194
+ * Gets difficulty progression advice.
195
+ * @returns Advice for progressing through difficulty levels
196
+ */
197
+ static getDifficultyProgression() {
198
+ return [
199
+ 'Master naked singles and hidden singles completely before advancing',
200
+ 'Beginner puzzles can often be solved with just these two techniques',
201
+ 'Intermediate puzzles introduce pointing pairs and box/line interactions',
202
+ 'Advanced puzzles require naked and hidden pairs/triples',
203
+ 'Expert puzzles may need advanced techniques like X-wing and swordfish',
204
+ 'Each technique builds on the foundation of simpler techniques'
205
+ ];
206
+ }
207
+ /**
208
+ * Creates a complete educational guide for beginners.
209
+ * @returns Comprehensive beginner guide
210
+ */
211
+ static createBeginnerGuide() {
212
+ const sections = [
213
+ 'SUDOKU SOLVING GUIDE FOR BEGINNERS',
214
+ '='.repeat(35),
215
+ '',
216
+ 'Sudoku is a logic puzzle where you fill a 9x9 grid with digits 1-9.',
217
+ 'Each row, column, and 3x3 box must contain each digit exactly once.',
218
+ '',
219
+ 'BASIC TECHNIQUES:',
220
+ '',
221
+ '1. Naked Singles',
222
+ this._techniqueIntroductions.get(TechniqueIds.NAKED_SINGLES),
223
+ '',
224
+ '2. Hidden Singles',
225
+ this._techniqueIntroductions.get(TechniqueIds.HIDDEN_SINGLES),
226
+ '',
227
+ 'SOLVING STRATEGY:',
228
+ ...this.getGeneralSolvingTips().map((tip) => `• ${tip}`),
229
+ '',
230
+ 'PROGRESSION:',
231
+ ...this.getDifficultyProgression().map((tip) => `• ${tip}`)
232
+ ];
233
+ return sections.join('\n');
234
+ }
235
+ }
236
+ EducationalContent._techniqueIntroductions = new Map([
237
+ [
238
+ TechniqueIds.NAKED_SINGLES,
239
+ 'Naked Singles are the most fundamental Sudoku technique. When a cell has only one possible ' +
240
+ 'value due to the numbers already placed in its row, column, and 3x3 box, that cell contains ' +
241
+ 'a "naked single." This technique is always safe to apply and forms the foundation for more ' +
242
+ 'advanced solving methods.'
243
+ ],
244
+ [
245
+ TechniqueIds.HIDDEN_SINGLES,
246
+ 'Hidden Singles focus on finding where specific values must be placed within a unit (row, ' +
247
+ 'column, or 3x3 box). When a particular number can only be placed in one cell within a unit, ' +
248
+ 'that placement is a "hidden single." This technique complements naked singles and together ' +
249
+ 'they can solve many easier puzzles completely.'
250
+ ]
251
+ ]);
252
+ EducationalContent._techniqueRelationships = new Map([
253
+ [
254
+ TechniqueIds.NAKED_SINGLES,
255
+ [
256
+ 'Apply naked singles first - they often reveal new naked singles',
257
+ 'Naked singles may create hidden singles in related units',
258
+ 'Use with hidden singles to solve beginner-level puzzles'
259
+ ]
260
+ ],
261
+ [
262
+ TechniqueIds.HIDDEN_SINGLES,
263
+ [
264
+ 'Check for hidden singles after applying naked singles',
265
+ 'Hidden singles can create new naked singles',
266
+ 'Consider all three unit types: rows, columns, and boxes'
267
+ ]
268
+ ]
269
+ ]);
270
+ //# sourceMappingURL=explanations.js.map
@@ -0,0 +1,233 @@
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 { BaseHintProvider } from './baseHintProvider';
26
+ import { ConfidenceLevels, TechniqueIds } from './types';
27
+ /**
28
+ * Hint provider for the Hidden Singles technique.
29
+ *
30
+ * A Hidden Single occurs when a value can only be placed in one cell within
31
+ * a row, column, or 3x3 box, even though that cell may have multiple candidates.
32
+ *
33
+ * @public
34
+ */
35
+ export class HiddenSinglesProvider extends BaseHintProvider {
36
+ /**
37
+ * Creates a new HiddenSinglesProvider instance.
38
+ */
39
+ constructor() {
40
+ super({
41
+ techniqueId: TechniqueIds.HIDDEN_SINGLES,
42
+ techniqueName: 'Hidden Singles',
43
+ difficulty: 'beginner',
44
+ priority: 2, // Second priority after naked singles
45
+ defaultConfidence: ConfidenceLevels.HIGH
46
+ });
47
+ }
48
+ /**
49
+ * Determines if this provider can potentially generate hints for the given puzzle.
50
+ * @param puzzle - The puzzle structure containing constraints
51
+ * @param state - The current puzzle state
52
+ * @returns true if there are empty cells that might have hidden singles
53
+ */
54
+ canProvideHints(puzzle, state) {
55
+ const emptyCells = this.getEmptyCells(puzzle, state);
56
+ return emptyCells.length > 0;
57
+ }
58
+ /**
59
+ * Generates all hidden single hints for the given puzzle.
60
+ * @param puzzle - The puzzle structure containing constraints
61
+ * @param state - The current puzzle state
62
+ * @param options - Optional generation options
63
+ * @returns Result containing array of hidden single hints
64
+ */
65
+ generateHints(puzzle, state, options) {
66
+ return this.validateOptions(options).onSuccess(() => {
67
+ const hints = [];
68
+ const hiddenSingles = this._findAllHiddenSingles(puzzle, state);
69
+ for (const hiddenSingle of hiddenSingles) {
70
+ const hint = this._createHiddenSingleHint(hiddenSingle, puzzle, state);
71
+ hints.push(hint);
72
+ }
73
+ return succeed(this.filterHints(hints, options));
74
+ });
75
+ }
76
+ /**
77
+ * Finds all hidden singles in the puzzle using cage-based analysis.
78
+ * @param puzzle - The puzzle structure containing constraints
79
+ * @param state - The current puzzle state
80
+ * @returns Array of hidden single information
81
+ */
82
+ _findAllHiddenSingles(puzzle, state) {
83
+ const hiddenSingles = [];
84
+ // Check all cages for hidden singles
85
+ for (const cage of puzzle.cages) {
86
+ hiddenSingles.push(...this._findHiddenSinglesInCage(cage, puzzle, state));
87
+ }
88
+ // Remove duplicates (a cell might be found as hidden single in multiple cages)
89
+ return this._removeDuplicateHiddenSingles(hiddenSingles);
90
+ }
91
+ /**
92
+ * Finds hidden singles in a specific cage using dynamic value analysis.
93
+ * @param cage - The cage to analyze
94
+ * @param puzzle - The puzzle structure containing constraints
95
+ * @param state - The current puzzle state
96
+ * @returns Array of hidden singles in this cage
97
+ */
98
+ _findHiddenSinglesInCage(cage, puzzle, state) {
99
+ const hiddenSingles = [];
100
+ const maxValue = Math.sqrt(puzzle.numRows * puzzle.numColumns);
101
+ // For each possible value, check if it can only go in one cell in this cage
102
+ for (let value = 1; value <= maxValue; value++) {
103
+ const possibleCells = [];
104
+ for (const cellId of cage.cellIds) {
105
+ const candidates = this.getCandidates(cellId, puzzle, state);
106
+ if (candidates.includes(value)) {
107
+ possibleCells.push(cellId);
108
+ }
109
+ }
110
+ // Hidden single: exactly one cell can contain this value
111
+ if (possibleCells.length === 1) {
112
+ const cellId = possibleCells[0];
113
+ const otherCandidateCells = cage.cellIds.filter((id) => id !== cellId && !state.hasValue(id));
114
+ hiddenSingles.push({
115
+ cellId,
116
+ value,
117
+ unitType: cage.cageType,
118
+ unitIndex: this._getCageDisplayIndex(cage),
119
+ otherCandidateCells
120
+ });
121
+ }
122
+ }
123
+ return hiddenSingles;
124
+ }
125
+ /**
126
+ * Gets a display-friendly index for a cage.
127
+ * @param cage - The cage to get index for
128
+ * @returns Display index for the cage
129
+ */
130
+ _getCageDisplayIndex(cage) {
131
+ // Extract numeric part from cage ID if available
132
+ const match = cage.id.match(/\d+/);
133
+ return match ? parseInt(match[0], 10) : 0;
134
+ }
135
+ /**
136
+ * Removes duplicate hidden singles (same cell and value).
137
+ * @param hiddenSingles - Array of potentially duplicate hidden singles
138
+ * @returns Array with duplicates removed
139
+ */
140
+ _removeDuplicateHiddenSingles(hiddenSingles) {
141
+ const seen = new Set();
142
+ const unique = [];
143
+ for (const hiddenSingle of hiddenSingles) {
144
+ const key = `${hiddenSingle.cellId}-${hiddenSingle.value}`;
145
+ if (!seen.has(key)) {
146
+ seen.add(key);
147
+ unique.push(hiddenSingle);
148
+ }
149
+ }
150
+ return unique;
151
+ }
152
+ /**
153
+ * Creates a hint for a specific hidden single.
154
+ * @param hiddenSingle - The hidden single information
155
+ * @param puzzle - The puzzle structure containing constraints
156
+ * @param state - The current puzzle state
157
+ * @returns A complete hint for this hidden single
158
+ */
159
+ _createHiddenSingleHint(hiddenSingle, __puzzle, __state) {
160
+ const { cellId, value, unitType, unitIndex } = hiddenSingle;
161
+ // Create cell action to set the value
162
+ const cellAction = this.createCellAction(cellId, 'set-value', value, `Only cell in ${unitType} ${unitIndex} that can contain ${value}`);
163
+ // Create relevant cells grouping
164
+ const relevantCells = this.createRelevantCells([cellId], // Primary: the cell with the hidden single
165
+ hiddenSingle.otherCandidateCells, // Secondary: other empty cells in the unit
166
+ [] // Affected: none for hidden singles
167
+ );
168
+ // Create explanations at different levels
169
+ const unitName = this._getUnitName(unitType, unitIndex);
170
+ const explanations = [
171
+ this.createExplanation('brief', 'Hidden Single', `In ${unitName}, the value ${value} can only go in cell ${cellId}.`, [`Set ${cellId} = ${value}`], ['Look for values that can only go in one cell within a unit']),
172
+ this.createExplanation('detailed', 'Hidden Single Analysis', `In ${unitName}, the value ${value} can only be placed in cell ${cellId}. While this cell ` +
173
+ `may have other candidates, ${value} has no other valid positions in this ${unitType}.`, [
174
+ `Examine all empty cells in ${unitName}`,
175
+ `For each empty cell, determine which values are possible candidates`,
176
+ `Check where the value ${value} can be placed`,
177
+ `Identify that ${value} can only go in ${cellId}`,
178
+ `Set ${cellId} = ${value}`
179
+ ], [
180
+ 'Hidden singles focus on where a value can go, not what can go in a cell',
181
+ 'Check each value 1-9 systematically within each unit',
182
+ 'A value with only one possible position must go there'
183
+ ]),
184
+ this.createExplanation('educational', 'Understanding Hidden Singles', `A hidden single occurs when a particular value can only be placed in one cell within a ` +
185
+ `row, column, or 3x3 box. In ${unitName}, we need to place the value ${value} somewhere, ` +
186
+ `but due to constraints from other filled cells, it can only go in ${cellId}. This technique ` +
187
+ `is called "hidden" because the single placement might not be obvious when looking at the ` +
188
+ `cell's candidates alone - you need to consider the entire unit.`, [
189
+ `Focus on ${unitName}`,
190
+ `Identify that this unit needs the value ${value} to be placed somewhere`,
191
+ `Examine each empty cell in this ${unitType} to see if ${value} could be placed there`,
192
+ `Consider the constraints from the cell's row, column, and box`,
193
+ `Eliminate cells where ${value} cannot go due to conflicts`,
194
+ `Confirm that ${cellId} is the only remaining option for ${value}`,
195
+ `Place ${value} in cell ${cellId}`
196
+ ], [
197
+ 'Hidden singles complement naked singles - use both techniques together',
198
+ 'Systematically check each value 1-9 in each row, column, and box',
199
+ 'This technique often reveals itself after placing other numbers',
200
+ 'Hidden singles can exist even when a cell has many candidates'
201
+ ])
202
+ ];
203
+ return this.createHint([cellAction], relevantCells, explanations, ConfidenceLevels.HIGH);
204
+ }
205
+ /**
206
+ * Gets a human-readable name for a unit.
207
+ * @param unitType - The type of unit
208
+ * @param unitIndex - The index of the unit
209
+ * @returns Human-readable unit name
210
+ */
211
+ _getUnitName(unitType, unitIndex) {
212
+ switch (unitType) {
213
+ case 'row':
214
+ return `row ${unitIndex + 1}`;
215
+ case 'column':
216
+ return `column ${unitIndex + 1}`;
217
+ /* c8 ignore next 4 - unreachable: puzzles use 'section' cage type, not 'box' */
218
+ case 'box':
219
+ const boxRow = Math.floor(unitIndex / 3);
220
+ const boxCol = unitIndex % 3;
221
+ return `box (${boxRow + 1},${boxCol + 1})`;
222
+ }
223
+ return `${unitType} ${unitIndex}`;
224
+ }
225
+ /**
226
+ * Static factory method to create a new HiddenSinglesProvider.
227
+ * @returns Result containing the new provider
228
+ */
229
+ static create() {
230
+ return succeed(new HiddenSinglesProvider());
231
+ }
232
+ }
233
+ //# sourceMappingURL=hiddenSingles.js.map