@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,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
|
package/dist/ts-sudoku-lib.d.ts
CHANGED
|
@@ -371,17 +371,12 @@ declare namespace Files {
|
|
|
371
371
|
export {
|
|
372
372
|
Converters_2 as Converters,
|
|
373
373
|
Model,
|
|
374
|
-
|
|
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.
|