@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,222 @@
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 { getCageTotalBounds } from '../common';
26
+ import { CombinationCache } from './internal/combinationCache';
27
+ import { CombinationGenerator } from './internal/combinationGenerator';
28
+ import { PossibilityAnalyzer } from './internal/possibilityAnalyzer';
29
+ /**
30
+ * Helper class providing UI assistance functions for killer sudoku puzzle solving.
31
+ * Generates possible totals, number combinations with constraints, and cell-specific
32
+ * possibilities based on current puzzle state.
33
+ * @public
34
+ */
35
+ export class KillerCombinations {
36
+ /**
37
+ * Gets all mathematically possible totals for a given cage size.
38
+ *
39
+ * Uses the existing totalsByCageSize constant to determine the valid range
40
+ * of totals for the specified cage size and returns all integers in that range.
41
+ *
42
+ * @param cageSize - The number of cells in the cage (must be 1-9)
43
+ * @returns Result containing array of possible totals in ascending order
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Get possible totals for a 3-cell cage
48
+ * const result = KillerCombinations.getPossibleTotals(3);
49
+ * if (result.isSuccess()) {
50
+ * console.log(result.value); // [6, 7, 8, 9, ..., 24]
51
+ * }
52
+ * ```
53
+ */
54
+ static getPossibleTotals(cageSize, maxValue = 9) {
55
+ // Validate cage size
56
+ if (!Number.isInteger(cageSize) || cageSize < 1 || cageSize > maxValue) {
57
+ return fail(`Cage size must be an integer between 1 and ${maxValue}, got ${cageSize}`);
58
+ }
59
+ const bounds = getCageTotalBounds(cageSize, maxValue);
60
+ // Generate all totals in the valid range
61
+ const totals = [];
62
+ for (let total = bounds.min; total <= bounds.max; total++) {
63
+ totals.push(total);
64
+ }
65
+ return succeed(totals);
66
+ }
67
+ /**
68
+ * Generates all possible number combinations that sum to the target total.
69
+ *
70
+ * Each combination contains unique numbers from 1-9 that sum exactly to the
71
+ * specified total. Combinations respect both excluded and required number
72
+ * constraints if provided.
73
+ *
74
+ * @param cageSize - The number of cells in the cage (must be 1-9)
75
+ * @param total - The target sum (must be valid for the cage size)
76
+ * @param constraints - Optional constraints on included/excluded numbers
77
+ * @returns Result containing array of combinations, each sorted in ascending order
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // Get all combinations for a 3-cell cage with total 15
82
+ * const result = KillerCombinations.getCombinations(3, 15);
83
+ * if (result.isSuccess()) {
84
+ * console.log(result.value); // [[1,5,9], [1,6,8], [2,4,9], ...]
85
+ * }
86
+ *
87
+ * // With constraints - exclude 1 and 2, require 9
88
+ * const constrained = KillerCombinations.getCombinations(3, 15, {
89
+ * excludedNumbers: [1, 2],
90
+ * requiredNumbers: [9]
91
+ * });
92
+ * ```
93
+ */
94
+ static getCombinations(cageSize, total, constraints, maxValue = 9) {
95
+ // Validate cage size
96
+ if (!Number.isInteger(cageSize) || cageSize < 1 || cageSize > maxValue) {
97
+ return fail(`Cage size must be an integer between 1 and ${maxValue}, got ${cageSize}`);
98
+ }
99
+ // Validate total against cage size bounds
100
+ const bounds = getCageTotalBounds(cageSize, maxValue);
101
+ if (!Number.isInteger(total) || total < bounds.min || total > bounds.max) {
102
+ return fail(`Total ${total} is invalid for cage size ${cageSize} - valid range: ${bounds.min}-${bounds.max}`);
103
+ }
104
+ // Validate constraints
105
+ const constraintValidation = this._validateConstraints(constraints, maxValue);
106
+ if (constraintValidation.isFailure()) {
107
+ return fail(constraintValidation.message);
108
+ }
109
+ // Check cache first
110
+ const cacheKey = CombinationCache.generateKey(cageSize, total, constraints);
111
+ const cached = CombinationCache.get(cacheKey);
112
+ if (cached) {
113
+ return succeed(cached);
114
+ }
115
+ // Generate combinations
116
+ const combinations = CombinationGenerator.generate(cageSize, total, constraints, maxValue);
117
+ // Cache the result
118
+ CombinationCache.set(cacheKey, combinations);
119
+ return succeed(combinations);
120
+ }
121
+ /**
122
+ * Determines possible values for each cell in a killer cage based on current puzzle state.
123
+ *
124
+ * Analyzes the current state of the puzzle and cage to determine which values
125
+ * are possible for each empty cell, considering both killer cage constraints
126
+ * and standard sudoku constraints (row, column, section uniqueness).
127
+ *
128
+ * @param puzzle - The puzzle instance
129
+ * @param state - Current puzzle state
130
+ * @param cage - The killer cage to analyze (must be of type 'killer')
131
+ * @returns Result containing map of CellId to possible number arrays
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const cage = puzzle.getCage(cageId).orThrow();
136
+ * const possibilities = KillerCombinations.getCellPossibilities(puzzle, state, cage);
137
+ * if (possibilities.isSuccess()) {
138
+ * for (const [cellId, values] of possibilities.value) {
139
+ * console.log(`Cell ${cellId} can have values: ${values.join(', ')}`);
140
+ * }
141
+ * }
142
+ * ```
143
+ */
144
+ static getCellPossibilities(puzzle, state, cage) {
145
+ // Validate cage type
146
+ if (cage.cageType !== 'killer') {
147
+ return fail(`Expected killer cage, got ${cage.cageType}`);
148
+ }
149
+ // Get current values in the cage to build constraints
150
+ // We need the concrete Cage class for containedValues method
151
+ const concreteCage = cage;
152
+ const currentValues = concreteCage.containedValues(state);
153
+ const constraints = {
154
+ excludedNumbers: Array.from(currentValues)
155
+ };
156
+ // Get valid combinations for this cage
157
+ const combinationsResult = this.getCombinations(cage.numCells, cage.total, constraints);
158
+ /* c8 ignore next 3 - error propagation from getCombinations already tested, integration error hard to trigger */
159
+ if (combinationsResult.isFailure()) {
160
+ return fail(`Failed to get combinations for cage ${cage.id}: ${combinationsResult.message}`);
161
+ }
162
+ // Analyze possibilities for each cell
163
+ return PossibilityAnalyzer.analyze(puzzle, state, cage, combinationsResult.value);
164
+ }
165
+ /**
166
+ * Validate constraint parameters.
167
+ * @param constraints - Constraints to validate
168
+ * @param maxValue - Maximum value allowed in cells
169
+ * @returns Result indicating success or failure with error message
170
+ */
171
+ static _validateConstraints(constraints, maxValue = 9) {
172
+ if (!constraints) {
173
+ return succeed(true);
174
+ }
175
+ // Validate excluded numbers
176
+ if (constraints.excludedNumbers) {
177
+ const excluded = constraints.excludedNumbers;
178
+ if (!Array.isArray(excluded)) {
179
+ return fail('excludedNumbers must be an array');
180
+ }
181
+ for (const num of excluded) {
182
+ if (!Number.isInteger(num) || num < 1 || num > maxValue) {
183
+ return fail(`excludedNumbers must contain integers between 1-${maxValue}, got ${num}`);
184
+ }
185
+ }
186
+ // Check for duplicates
187
+ const uniqueExcluded = new Set(excluded);
188
+ if (uniqueExcluded.size !== excluded.length) {
189
+ return fail('excludedNumbers must not contain duplicates');
190
+ }
191
+ }
192
+ // Validate required numbers
193
+ if (constraints.requiredNumbers) {
194
+ const required = constraints.requiredNumbers;
195
+ if (!Array.isArray(required)) {
196
+ return fail('requiredNumbers must be an array');
197
+ }
198
+ for (const num of required) {
199
+ if (!Number.isInteger(num) || num < 1 || num > maxValue) {
200
+ return fail(`requiredNumbers must contain integers between 1-${maxValue}, got ${num}`);
201
+ }
202
+ }
203
+ // Check for duplicates
204
+ const uniqueRequired = new Set(required);
205
+ if (uniqueRequired.size !== required.length) {
206
+ return fail('requiredNumbers must not contain duplicates');
207
+ }
208
+ }
209
+ // Check for overlap between excluded and required
210
+ if (constraints.excludedNumbers && constraints.requiredNumbers) {
211
+ const excludedSet = new Set(constraints.excludedNumbers);
212
+ const requiredSet = new Set(constraints.requiredNumbers);
213
+ for (const required of requiredSet) {
214
+ if (excludedSet.has(required)) {
215
+ return fail(`Number ${required} cannot be both required and excluded`);
216
+ }
217
+ }
218
+ }
219
+ return succeed(true);
220
+ }
221
+ }
222
+ //# sourceMappingURL=killerCombinations.js.map
@@ -0,0 +1,25 @@
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
+ export {};
25
+ //# sourceMappingURL=killerCombinationsTypes.js.map
@@ -0,0 +1,136 @@
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 { captureResult, fail, succeed } from '@fgv/ts-utils';
25
+ import { Cage, Ids, Puzzle, getCageTotalBounds } from '../common';
26
+ const cageDefFormat = /^[A-Za-z][0-9][0-9]$/;
27
+ /**
28
+ * @public
29
+ */
30
+ export class KillerSudokuPuzzle extends Puzzle {
31
+ constructor(puzzle, cages) {
32
+ super(puzzle, cages);
33
+ }
34
+ static create(desc) {
35
+ /* c8 ignore next 3 */
36
+ if (desc.type !== 'killer-sudoku') {
37
+ return fail(`Puzzle '${desc.description}' unsupported type ${desc.type}`);
38
+ }
39
+ return captureResult(() => {
40
+ const { cages, givens } = KillerSudokuPuzzle._getKillerCages(desc);
41
+ const cells = KillerSudokuPuzzle._getKillerCells(desc, givens);
42
+ return new KillerSudokuPuzzle(Object.assign(Object.assign({}, desc), { cells }), cages);
43
+ })
44
+ .onSuccess((puzzle) => {
45
+ return succeed(puzzle);
46
+ })
47
+ .onFailure((message) => {
48
+ return fail(`Failed to load killer puzzle "${desc.description}" - ${message}`);
49
+ });
50
+ }
51
+ static _getKillerCages(puzzle) {
52
+ const decl = puzzle.cells.split('|');
53
+ /* c8 ignore next 3 - tested but coverage has intermittent issues */
54
+ if (decl.length !== 2) {
55
+ throw new Error(`malformed cells|cages "${puzzle.cells}"`);
56
+ }
57
+ const cageCells = KillerSudokuPuzzle._getCageCells(puzzle, decl[0]);
58
+ const allCages = KillerSudokuPuzzle._getCages(puzzle, cageCells, decl[1]);
59
+ const cages = allCages.filter(([__id, cage]) => cage.numCells > 1);
60
+ const givens = allCages.filter(([__id, cage]) => cage.numCells === 1).map(([__id, cage]) => cage);
61
+ return { cages, givens };
62
+ }
63
+ static _getCageCells(puzzle, mappingDecl) {
64
+ var _a;
65
+ const cages = new Map();
66
+ const cageMapping = Array.from(mappingDecl);
67
+ /* c8 ignore next 5 - defensive coding: protected by validation in puzzleDefinitions.ts */
68
+ if (cageMapping.length !== puzzle.totalRows * puzzle.totalColumns) {
69
+ const expected = puzzle.totalRows * puzzle.totalColumns;
70
+ const got = cageMapping.length;
71
+ throw new Error(`expected ${expected} cell mappings, found ${got}`);
72
+ }
73
+ for (let row = 0; row < puzzle.totalRows; row++) {
74
+ for (let col = 0; col < puzzle.totalColumns; col++) {
75
+ const cage = cageMapping.shift();
76
+ const cell = Ids.cellId({ row, col }).orThrow();
77
+ const cells = (_a = cages.get(cage)) !== null && _a !== void 0 ? _a : [];
78
+ cells.push(cell);
79
+ cages.set(cage, cells);
80
+ }
81
+ }
82
+ return cages;
83
+ }
84
+ static _getCages(puzzle, cageCells, cagePart) {
85
+ const cages = new Map();
86
+ const cageDefs = cagePart.split(',');
87
+ if (cageDefs.length !== cageCells.size) {
88
+ throw new Error(`expected ${cageCells.size} cage sizes, found ${cageDefs.length}`);
89
+ }
90
+ for (const def of cageDefs) {
91
+ if (!cageDefFormat.test(def)) {
92
+ throw new Error(`malformed cage spec ${def}`);
93
+ }
94
+ const cage = def.slice(0, 1);
95
+ const cageId = Ids.cageId(`K${cage}`).orThrow();
96
+ const total = Number.parseInt(def.slice(1));
97
+ const cells = cageCells.get(cage);
98
+ /* c8 ignore next 3 - should never happen */
99
+ if (!cells) {
100
+ throw new Error(`cage ${cageId} has no cells`);
101
+ }
102
+ // Use the puzzle's maxValue for dynamic validation
103
+ const maxValue = puzzle.maxValue;
104
+ if (cells.length < 1 || cells.length > maxValue) {
105
+ throw new Error(`invalid cell count ${cells.length} for cage ${cageId} (max ${maxValue})`);
106
+ }
107
+ const { min, max } = getCageTotalBounds(cells.length, maxValue);
108
+ if (total < min || total > max) {
109
+ throw new Error(`invalid total ${total} for cage ${cageId} (expected ${min}..${max})`);
110
+ }
111
+ cages.set(cageId, Cage.create(cageId, 'killer', total, cells).orThrow());
112
+ }
113
+ return Array.from(cages.entries());
114
+ }
115
+ static _getKillerCells(puzzle, givens) {
116
+ const cells = [];
117
+ for (let row = 0; row < puzzle.totalRows; row++) {
118
+ for (let col = 0; col < puzzle.totalColumns; col++) {
119
+ const cellId = Ids.cellId({ row, col }).orThrow();
120
+ const cage = givens.find((g) => g.cellIds[0] === cellId);
121
+ if (cage) {
122
+ /* c8 ignore next 3 - defense in depth should never happen */
123
+ if (cage.total < 1 || cage.total > puzzle.maxValue) {
124
+ throw new Error(`invalid total ${cage.total} for cell ${cellId}`);
125
+ }
126
+ cells.push(String(cage.total));
127
+ }
128
+ else {
129
+ cells.push('.');
130
+ }
131
+ }
132
+ }
133
+ return cells.join('');
134
+ }
135
+ }
136
+ //# sourceMappingURL=killerSudokuPuzzle.js.map
@@ -0,0 +1,43 @@
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 { captureResult, fail, succeed } from '@fgv/ts-utils';
25
+ import { Puzzle } from '../common';
26
+ /**
27
+ * @public
28
+ */
29
+ export class SudokuPuzzle extends Puzzle {
30
+ constructor(puzzle) {
31
+ super(puzzle);
32
+ }
33
+ static create(puzzle) {
34
+ /* c8 ignore next 3 */
35
+ if (puzzle.type !== 'sudoku') {
36
+ return fail(`Puzzle '${puzzle.description}' unsupported type ${puzzle.type}`);
37
+ }
38
+ return captureResult(() => new SudokuPuzzle(puzzle)).onSuccess((puzzle) => {
39
+ return succeed(puzzle);
40
+ });
41
+ }
42
+ }
43
+ //# sourceMappingURL=sudokuPuzzle.js.map
@@ -0,0 +1,65 @@
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 { captureResult, succeed, fail } from '@fgv/ts-utils';
25
+ import { Cage, Ids, Puzzle } from '../common';
26
+ /**
27
+ * @public
28
+ */
29
+ export class SudokuXPuzzle extends Puzzle {
30
+ constructor(puzzle, extraCages) {
31
+ super(puzzle, extraCages);
32
+ }
33
+ static create(puzzle) {
34
+ /* c8 ignore next 3 */
35
+ if (puzzle.type !== 'sudoku-x') {
36
+ return fail(`Puzzle '${puzzle.description}' unsupported type ${puzzle.type}`);
37
+ }
38
+ // Sudoku X diagonals work for any square puzzle (totalRows === totalColumns)
39
+ if (puzzle.totalRows !== puzzle.totalColumns) {
40
+ return fail(`Sudoku X puzzle must be square, got ${puzzle.totalRows}x${puzzle.totalColumns}`);
41
+ }
42
+ return captureResult(() => {
43
+ return new SudokuXPuzzle(puzzle, SudokuXPuzzle._getXCages(puzzle));
44
+ }).onSuccess((puzzle) => {
45
+ return succeed(puzzle);
46
+ });
47
+ }
48
+ static _getXCages(puzzle) {
49
+ const x1Cells = [];
50
+ const x2Cells = [];
51
+ for (let row = 0, col1 = 0, col2 = puzzle.totalColumns - 1; row < puzzle.totalRows; row++, col1++, col2--) {
52
+ x1Cells.push(Ids.cellId({ row, col: col1 }).orThrow());
53
+ x2Cells.push(Ids.cellId({ row, col: col2 }).orThrow());
54
+ }
55
+ const x1Id = Ids.cageId('X1').orThrow();
56
+ const x2Id = Ids.cageId('X2').orThrow();
57
+ const x1 = Cage.create(x1Id, 'x', puzzle.basicCageTotal, x1Cells).orThrow();
58
+ const x2 = Cage.create(x2Id, 'x', puzzle.basicCageTotal, x2Cells).orThrow();
59
+ return [
60
+ [x1Id, x1],
61
+ [x2Id, x2]
62
+ ];
63
+ }
64
+ }
65
+ //# sourceMappingURL=sudokuXPuzzle.js.map
@@ -0,0 +1,149 @@
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 '@fgv/ts-utils-jest';
25
+ import { PuzzleSession, PuzzleDefinitionFactory, STANDARD_CONFIGS } from '../../../index';
26
+ import { Puzzles } from '../../../index';
27
+ /**
28
+ * Creates a standard test puzzle and state from row strings
29
+ * @param rows - 9 strings representing puzzle rows (use '.' for empty cells)
30
+ * @param puzzleId - Optional puzzle ID (defaults to 'test-puzzle')
31
+ * @returns Puzzle instance and initial state
32
+ */
33
+ export function createPuzzleAndState(rows, puzzleId = 'test-puzzle') {
34
+ const puzzleDefinition = PuzzleDefinitionFactory.create(STANDARD_CONFIGS.puzzle9x9, {
35
+ id: puzzleId,
36
+ description: 'Test puzzle',
37
+ type: 'sudoku',
38
+ level: 1,
39
+ cells: rows.join('')
40
+ }).orThrow();
41
+ const puzzle = Puzzles.Any.create(puzzleDefinition).orThrow();
42
+ const session = PuzzleSession.create(puzzle).orThrow();
43
+ return { puzzle, state: session.state };
44
+ }
45
+ /**
46
+ * Creates a test puzzle with specific dimensions
47
+ * @param dimensions - Puzzle dimensions configuration
48
+ * @param cells - Cell values string
49
+ * @param type - Puzzle type (defaults to 'sudoku')
50
+ * @returns Puzzle instance and initial state
51
+ */
52
+ export function createPuzzleWithDimensions(dimensions, cells, type = 'sudoku') {
53
+ const puzzleDefinition = PuzzleDefinitionFactory.create(dimensions, {
54
+ id: 'test-puzzle',
55
+ description: 'Test puzzle',
56
+ type,
57
+ level: 1,
58
+ cells
59
+ }).orThrow();
60
+ const puzzle = Puzzles.Any.create(puzzleDefinition).orThrow();
61
+ const session = PuzzleSession.create(puzzle).orThrow();
62
+ return { puzzle, state: session.state };
63
+ }
64
+ /**
65
+ * Creates a killer sudoku puzzle for testing cage-based logic
66
+ * @param config - Configuration for the killer puzzle
67
+ * @returns Puzzle instance, session, and first cage
68
+ */
69
+ export function createTestKillerPuzzle(config) {
70
+ var _a;
71
+ const cells = (_a = config.cells) !== null && _a !== void 0 ? _a : '.'.repeat(81);
72
+ // Build cage specifications
73
+ let cageSpecs = '';
74
+ if (config.cages) {
75
+ cageSpecs = config.cages
76
+ .map((cage) => {
77
+ var _a;
78
+ const id = (_a = cage.id) !== null && _a !== void 0 ? _a : `cage${cage.sum}`;
79
+ const cellList = cage.cells.join(',');
80
+ return `${id}:${cellList}:${cage.sum}`;
81
+ })
82
+ .join('|');
83
+ }
84
+ const puzzleDefinition = PuzzleDefinitionFactory.create(STANDARD_CONFIGS.puzzle9x9, {
85
+ id: 'test-killer',
86
+ description: 'Test Killer Sudoku',
87
+ type: 'killer',
88
+ level: 1,
89
+ cells: cells + (cageSpecs ? `:${cageSpecs}` : '')
90
+ }).orThrow();
91
+ const puzzle = Puzzles.Killer.create(puzzleDefinition).orThrow();
92
+ const session = PuzzleSession.create(puzzle).orThrow();
93
+ // Return first cage if any were defined
94
+ const cage = config.cages && config.cages.length > 0
95
+ ? puzzle.cages.find((c) => { var _a; return c.id === ((_a = config.cages[0].id) !== null && _a !== void 0 ? _a : `cage${config.cages[0].sum}`); })
96
+ : undefined;
97
+ return { puzzle, session, cage };
98
+ }
99
+ /**
100
+ * Creates a simple killer sudoku cage for testing
101
+ * @param cageSize - Number of cells in the cage
102
+ * @param total - Sum total for the cage
103
+ * @param startCell - Starting cell ID (defaults to 'A1')
104
+ * @returns Cage specification object
105
+ */
106
+ export function createSimpleKillerCage(cageSize, total, startCell = 'A1') {
107
+ const cells = [startCell];
108
+ // Add adjacent cells horizontally
109
+ const startRow = startCell.charCodeAt(0);
110
+ const startCol = parseInt(startCell.substring(1), 10);
111
+ for (let i = 1; i < cageSize && cells.length < cageSize; i++) {
112
+ const row = String.fromCharCode(startRow);
113
+ const col = startCol + i;
114
+ if (col <= 9) {
115
+ cells.push(`${row}${col}`);
116
+ }
117
+ }
118
+ // If we need more cells, add them vertically
119
+ for (let i = 1; cells.length < cageSize && i < 9; i++) {
120
+ const row = String.fromCharCode(startRow + i);
121
+ const col = startCol;
122
+ if (row <= 'I') {
123
+ cells.push(`${row}${col}`);
124
+ }
125
+ }
126
+ return {
127
+ id: `cage${total}`,
128
+ cells,
129
+ sum: total
130
+ };
131
+ }
132
+ /**
133
+ * Creates a sudoku X puzzle for testing diagonal constraints
134
+ * @param rows - 9 strings representing puzzle rows
135
+ * @returns Puzzle instance and initial state
136
+ */
137
+ export function createSudokuXPuzzle(rows) {
138
+ const puzzleDefinition = PuzzleDefinitionFactory.create(STANDARD_CONFIGS.puzzle9x9, {
139
+ id: 'test-sudoku-x',
140
+ description: 'Test Sudoku X',
141
+ type: 'sudoku-x',
142
+ level: 1,
143
+ cells: rows.join('')
144
+ }).orThrow();
145
+ const puzzle = Puzzles.SudokuX.create(puzzleDefinition).orThrow();
146
+ const session = PuzzleSession.create(puzzle).orThrow();
147
+ return { puzzle, state: session.state };
148
+ }
149
+ //# sourceMappingURL=puzzleBuilders.js.map
@@ -371,17 +371,12 @@ declare namespace Files {
371
371
  export {
372
372
  Converters_2 as Converters,
373
373
  Model,
374
- FileSystem_2 as FileSystem
374
+ loadJsonPuzzlesFileSync,
375
+ loadJsonPuzzlesFromTree
375
376
  }
376
377
  }
377
378
  export { Files }
378
379
 
379
- declare namespace FileSystem_2 {
380
- export {
381
- loadJsonPuzzlesFileSync
382
- }
383
- }
384
-
385
380
  /**
386
381
  * The minimum and maximum possible values for a {@link ICage | cage}, by cage size in
387
382
  * {@link ICell | cells}.
@@ -1263,6 +1258,16 @@ declare class KillerSudokuPuzzle extends Puzzle {
1263
1258
  */
1264
1259
  declare function loadJsonPuzzlesFileSync(path: string): Result<IPuzzlesFile>;
1265
1260
 
1261
+ /**
1262
+ * Loads a puzzles file from a {@link FileTree.FileTree | FileTree}.
1263
+ * @param fileTree - FileTree containing the file
1264
+ * @param filePath - Path within the FileTree to the puzzles file
1265
+ * @returns `Success` with the resulting file, or `Failure` with details if an
1266
+ * error occurs.
1267
+ * @public
1268
+ */
1269
+ declare function loadJsonPuzzlesFromTree(fileTree: FileTree.FileTree, filePath: string): Result<IPuzzlesFile>;
1270
+
1266
1271
  /**
1267
1272
  * Loads a puzzles file from a `IFileTreeFileItem`.
1268
1273
  * @param file - The `IFileTreeFileItem` to load.