@fgv/ts-sudoku-lib 5.0.1-0 → 5.0.1-2
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/debug_killer.js +50 -0
- package/debug_test.js +88 -0
- package/dist/ts-sudoku-lib.d.ts +1505 -42
- package/lib/index.d.ts +3 -2
- package/lib/index.js +5 -3
- package/lib/packlets/collections/collections.d.ts +15 -8
- package/lib/packlets/collections/collections.js +41 -11
- package/lib/packlets/collections/data/puzzles.json +28 -33
- package/lib/packlets/common/common.d.ts +15 -0
- package/lib/packlets/common/common.js +25 -0
- package/lib/packlets/common/converters.d.ts +0 -6
- package/lib/packlets/common/converters.js +17 -22
- package/lib/packlets/common/ids.d.ts +8 -1
- package/lib/packlets/common/ids.js +83 -10
- package/lib/packlets/common/index.d.ts +3 -2
- package/lib/packlets/common/index.js +5 -2
- package/lib/packlets/common/logging.d.ts +7 -0
- package/lib/packlets/common/logging.js +35 -0
- package/lib/packlets/common/puzzle.d.ts +23 -13
- package/lib/packlets/common/puzzle.js +51 -25
- package/lib/packlets/common/puzzleDefinitions.d.ts +127 -0
- package/lib/packlets/common/puzzleDefinitions.js +251 -0
- package/lib/packlets/common/puzzleSession.d.ts +8 -0
- package/lib/packlets/common/puzzleSession.js +12 -0
- package/lib/packlets/common/puzzleState.js +3 -0
- package/lib/packlets/files/converters.d.ts +27 -0
- package/lib/packlets/files/converters.js +71 -0
- package/lib/packlets/{file/converters.d.ts → files/filesystem.d.ts} +3 -8
- package/lib/packlets/{file/converters.js → files/filesystem.js} +5 -14
- package/lib/packlets/files/index.d.ts +5 -0
- package/lib/packlets/{file → files}/index.js +3 -1
- package/lib/packlets/files/model.d.ts +32 -0
- package/lib/packlets/hints/baseHintProvider.d.ts +103 -0
- package/lib/packlets/hints/baseHintProvider.js +203 -0
- package/lib/packlets/hints/explanations.d.ts +105 -0
- package/lib/packlets/hints/explanations.js +276 -0
- package/lib/packlets/hints/hiddenSingles.d.ts +81 -0
- package/lib/packlets/hints/hiddenSingles.js +237 -0
- package/lib/packlets/hints/hintRegistry.d.ts +95 -0
- package/lib/packlets/hints/hintRegistry.js +226 -0
- package/lib/packlets/hints/hints.d.ts +144 -0
- package/lib/packlets/hints/hints.js +316 -0
- package/lib/packlets/hints/index.d.ts +10 -0
- package/lib/packlets/hints/index.js +55 -0
- package/lib/packlets/hints/interfaces.d.ts +121 -0
- package/lib/packlets/{file/model.js → hints/interfaces.js} +1 -1
- package/lib/packlets/hints/nakedSingles.d.ts +57 -0
- package/lib/packlets/hints/nakedSingles.js +202 -0
- package/lib/packlets/hints/puzzleSessionHints.d.ts +306 -0
- package/lib/packlets/hints/puzzleSessionHints.js +499 -0
- package/lib/packlets/hints/types.d.ts +102 -0
- package/lib/packlets/hints/types.js +46 -0
- package/lib/packlets/puzzles/anyPuzzle.d.ts +3 -3
- package/lib/packlets/puzzles/anyPuzzle.js +1 -1
- package/lib/packlets/puzzles/index.d.ts +4 -2
- package/lib/packlets/puzzles/index.js +20 -3
- package/lib/packlets/puzzles/internal/combinationCache.d.ts +29 -0
- package/lib/packlets/puzzles/internal/combinationCache.js +79 -0
- package/lib/packlets/puzzles/internal/combinationGenerator.d.ts +34 -0
- package/lib/packlets/puzzles/internal/combinationGenerator.js +146 -0
- package/lib/packlets/puzzles/internal/possibilityAnalyzer.d.ts +25 -0
- package/lib/packlets/puzzles/internal/possibilityAnalyzer.js +125 -0
- package/lib/packlets/puzzles/killerCombinations.d.ts +90 -0
- package/lib/packlets/puzzles/killerCombinations.js +226 -0
- package/lib/packlets/puzzles/killerCombinationsTypes.d.ts +17 -0
- package/lib/packlets/puzzles/killerCombinationsTypes.js +26 -0
- package/lib/packlets/puzzles/killerSudokuPuzzle.d.ts +2 -2
- package/lib/packlets/puzzles/killerSudokuPuzzle.js +15 -11
- package/lib/packlets/puzzles/sudokuPuzzle.d.ts +2 -2
- package/lib/packlets/puzzles/sudokuXPuzzle.d.ts +2 -2
- package/lib/packlets/puzzles/sudokuXPuzzle.js +8 -4
- package/package.json +5 -5
- package/sample-12x12-puzzle.js +72 -0
- package/sample-4x4-puzzles.js +68 -0
- package/sample-6x6-puzzle.js +61 -0
- package/temp_12x12_cells.txt +12 -0
- package/temp_correct.txt +12 -0
- package/test-grid-sizes.js +132 -0
- package/lib/packlets/common/model.d.ts +0 -15
- package/lib/packlets/file/index.d.ts +0 -4
- package/lib/packlets/file/model.d.ts +0 -9
- /package/lib/packlets/{common → files}/model.js +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* MIT License
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2023 Erik Fortune
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
* copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
* SOFTWARE.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.KillerCombinations = void 0;
|
|
27
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
28
|
+
const common_1 = require("../common");
|
|
29
|
+
const combinationCache_1 = require("./internal/combinationCache");
|
|
30
|
+
const combinationGenerator_1 = require("./internal/combinationGenerator");
|
|
31
|
+
const possibilityAnalyzer_1 = require("./internal/possibilityAnalyzer");
|
|
32
|
+
/**
|
|
33
|
+
* Helper class providing UI assistance functions for killer sudoku puzzle solving.
|
|
34
|
+
* Generates possible totals, number combinations with constraints, and cell-specific
|
|
35
|
+
* possibilities based on current puzzle state.
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
class KillerCombinations {
|
|
39
|
+
/**
|
|
40
|
+
* Gets all mathematically possible totals for a given cage size.
|
|
41
|
+
*
|
|
42
|
+
* Uses the existing totalsByCageSize constant to determine the valid range
|
|
43
|
+
* of totals for the specified cage size and returns all integers in that range.
|
|
44
|
+
*
|
|
45
|
+
* @param cageSize - The number of cells in the cage (must be 1-9)
|
|
46
|
+
* @returns Result containing array of possible totals in ascending order
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // Get possible totals for a 3-cell cage
|
|
51
|
+
* const result = KillerCombinations.getPossibleTotals(3);
|
|
52
|
+
* if (result.isSuccess()) {
|
|
53
|
+
* console.log(result.value); // [6, 7, 8, 9, ..., 24]
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
static getPossibleTotals(cageSize, maxValue = 9) {
|
|
58
|
+
// Validate cage size
|
|
59
|
+
if (!Number.isInteger(cageSize) || cageSize < 1 || cageSize > maxValue) {
|
|
60
|
+
return (0, ts_utils_1.fail)(`Cage size must be an integer between 1 and ${maxValue}, got ${cageSize}`);
|
|
61
|
+
}
|
|
62
|
+
const bounds = (0, common_1.getCageTotalBounds)(cageSize, maxValue);
|
|
63
|
+
// Generate all totals in the valid range
|
|
64
|
+
const totals = [];
|
|
65
|
+
for (let total = bounds.min; total <= bounds.max; total++) {
|
|
66
|
+
totals.push(total);
|
|
67
|
+
}
|
|
68
|
+
return (0, ts_utils_1.succeed)(totals);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Generates all possible number combinations that sum to the target total.
|
|
72
|
+
*
|
|
73
|
+
* Each combination contains unique numbers from 1-9 that sum exactly to the
|
|
74
|
+
* specified total. Combinations respect both excluded and required number
|
|
75
|
+
* constraints if provided.
|
|
76
|
+
*
|
|
77
|
+
* @param cageSize - The number of cells in the cage (must be 1-9)
|
|
78
|
+
* @param total - The target sum (must be valid for the cage size)
|
|
79
|
+
* @param constraints - Optional constraints on included/excluded numbers
|
|
80
|
+
* @returns Result containing array of combinations, each sorted in ascending order
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* // Get all combinations for a 3-cell cage with total 15
|
|
85
|
+
* const result = KillerCombinations.getCombinations(3, 15);
|
|
86
|
+
* if (result.isSuccess()) {
|
|
87
|
+
* console.log(result.value); // [[1,5,9], [1,6,8], [2,4,9], ...]
|
|
88
|
+
* }
|
|
89
|
+
*
|
|
90
|
+
* // With constraints - exclude 1 and 2, require 9
|
|
91
|
+
* const constrained = KillerCombinations.getCombinations(3, 15, {
|
|
92
|
+
* excludedNumbers: [1, 2],
|
|
93
|
+
* requiredNumbers: [9]
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
static getCombinations(cageSize, total, constraints, maxValue = 9) {
|
|
98
|
+
// Validate cage size
|
|
99
|
+
if (!Number.isInteger(cageSize) || cageSize < 1 || cageSize > maxValue) {
|
|
100
|
+
return (0, ts_utils_1.fail)(`Cage size must be an integer between 1 and ${maxValue}, got ${cageSize}`);
|
|
101
|
+
}
|
|
102
|
+
// Validate total against cage size bounds
|
|
103
|
+
const bounds = (0, common_1.getCageTotalBounds)(cageSize, maxValue);
|
|
104
|
+
if (!Number.isInteger(total) || total < bounds.min || total > bounds.max) {
|
|
105
|
+
return (0, ts_utils_1.fail)(`Total ${total} is invalid for cage size ${cageSize} - valid range: ${bounds.min}-${bounds.max}`);
|
|
106
|
+
}
|
|
107
|
+
// Validate constraints
|
|
108
|
+
const constraintValidation = this._validateConstraints(constraints, maxValue);
|
|
109
|
+
if (constraintValidation.isFailure()) {
|
|
110
|
+
return (0, ts_utils_1.fail)(constraintValidation.message);
|
|
111
|
+
}
|
|
112
|
+
// Check cache first
|
|
113
|
+
const cacheKey = combinationCache_1.CombinationCache.generateKey(cageSize, total, constraints);
|
|
114
|
+
const cached = combinationCache_1.CombinationCache.get(cacheKey);
|
|
115
|
+
if (cached) {
|
|
116
|
+
return (0, ts_utils_1.succeed)(cached);
|
|
117
|
+
}
|
|
118
|
+
// Generate combinations
|
|
119
|
+
const combinations = combinationGenerator_1.CombinationGenerator.generate(cageSize, total, constraints, maxValue);
|
|
120
|
+
// Cache the result
|
|
121
|
+
combinationCache_1.CombinationCache.set(cacheKey, combinations);
|
|
122
|
+
return (0, ts_utils_1.succeed)(combinations);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Determines possible values for each cell in a killer cage based on current puzzle state.
|
|
126
|
+
*
|
|
127
|
+
* Analyzes the current state of the puzzle and cage to determine which values
|
|
128
|
+
* are possible for each empty cell, considering both killer cage constraints
|
|
129
|
+
* and standard sudoku constraints (row, column, section uniqueness).
|
|
130
|
+
*
|
|
131
|
+
* @param puzzle - The puzzle instance
|
|
132
|
+
* @param state - Current puzzle state
|
|
133
|
+
* @param cage - The killer cage to analyze (must be of type 'killer')
|
|
134
|
+
* @returns Result containing map of CellId to possible number arrays
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const cage = puzzle.getCage(cageId).orThrow();
|
|
139
|
+
* const possibilities = KillerCombinations.getCellPossibilities(puzzle, state, cage);
|
|
140
|
+
* if (possibilities.isSuccess()) {
|
|
141
|
+
* for (const [cellId, values] of possibilities.value) {
|
|
142
|
+
* console.log(`Cell ${cellId} can have values: ${values.join(', ')}`);
|
|
143
|
+
* }
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
static getCellPossibilities(puzzle, state, cage) {
|
|
148
|
+
// Validate cage type
|
|
149
|
+
if (cage.cageType !== 'killer') {
|
|
150
|
+
return (0, ts_utils_1.fail)(`Expected killer cage, got ${cage.cageType}`);
|
|
151
|
+
}
|
|
152
|
+
// Get current values in the cage to build constraints
|
|
153
|
+
// We need the concrete Cage class for containedValues method
|
|
154
|
+
const concreteCage = cage;
|
|
155
|
+
const currentValues = concreteCage.containedValues(state);
|
|
156
|
+
const constraints = {
|
|
157
|
+
excludedNumbers: Array.from(currentValues)
|
|
158
|
+
};
|
|
159
|
+
// Get valid combinations for this cage
|
|
160
|
+
const combinationsResult = this.getCombinations(cage.numCells, cage.total, constraints);
|
|
161
|
+
/* c8 ignore next 3 - error propagation from getCombinations already tested, integration error hard to trigger */
|
|
162
|
+
if (combinationsResult.isFailure()) {
|
|
163
|
+
return (0, ts_utils_1.fail)(`Failed to get combinations for cage ${cage.id}: ${combinationsResult.message}`);
|
|
164
|
+
}
|
|
165
|
+
// Analyze possibilities for each cell
|
|
166
|
+
return possibilityAnalyzer_1.PossibilityAnalyzer.analyze(puzzle, state, cage, combinationsResult.value);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validate constraint parameters.
|
|
170
|
+
* @param constraints - Constraints to validate
|
|
171
|
+
* @param maxValue - Maximum value allowed in cells
|
|
172
|
+
* @returns Result indicating success or failure with error message
|
|
173
|
+
*/
|
|
174
|
+
static _validateConstraints(constraints, maxValue = 9) {
|
|
175
|
+
if (!constraints) {
|
|
176
|
+
return (0, ts_utils_1.succeed)(true);
|
|
177
|
+
}
|
|
178
|
+
// Validate excluded numbers
|
|
179
|
+
if (constraints.excludedNumbers) {
|
|
180
|
+
const excluded = constraints.excludedNumbers;
|
|
181
|
+
if (!Array.isArray(excluded)) {
|
|
182
|
+
return (0, ts_utils_1.fail)('excludedNumbers must be an array');
|
|
183
|
+
}
|
|
184
|
+
for (const num of excluded) {
|
|
185
|
+
if (!Number.isInteger(num) || num < 1 || num > maxValue) {
|
|
186
|
+
return (0, ts_utils_1.fail)(`excludedNumbers must contain integers between 1-${maxValue}, got ${num}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Check for duplicates
|
|
190
|
+
const uniqueExcluded = new Set(excluded);
|
|
191
|
+
if (uniqueExcluded.size !== excluded.length) {
|
|
192
|
+
return (0, ts_utils_1.fail)('excludedNumbers must not contain duplicates');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Validate required numbers
|
|
196
|
+
if (constraints.requiredNumbers) {
|
|
197
|
+
const required = constraints.requiredNumbers;
|
|
198
|
+
if (!Array.isArray(required)) {
|
|
199
|
+
return (0, ts_utils_1.fail)('requiredNumbers must be an array');
|
|
200
|
+
}
|
|
201
|
+
for (const num of required) {
|
|
202
|
+
if (!Number.isInteger(num) || num < 1 || num > maxValue) {
|
|
203
|
+
return (0, ts_utils_1.fail)(`requiredNumbers must contain integers between 1-${maxValue}, got ${num}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Check for duplicates
|
|
207
|
+
const uniqueRequired = new Set(required);
|
|
208
|
+
if (uniqueRequired.size !== required.length) {
|
|
209
|
+
return (0, ts_utils_1.fail)('requiredNumbers must not contain duplicates');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Check for overlap between excluded and required
|
|
213
|
+
if (constraints.excludedNumbers && constraints.requiredNumbers) {
|
|
214
|
+
const excludedSet = new Set(constraints.excludedNumbers);
|
|
215
|
+
const requiredSet = new Set(constraints.requiredNumbers);
|
|
216
|
+
for (const required of requiredSet) {
|
|
217
|
+
if (excludedSet.has(required)) {
|
|
218
|
+
return (0, ts_utils_1.fail)(`Number ${required} cannot be both required and excluded`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return (0, ts_utils_1.succeed)(true);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
exports.KillerCombinations = KillerCombinations;
|
|
226
|
+
//# sourceMappingURL=killerCombinations.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constraints that can be applied when generating killer cage combinations.
|
|
3
|
+
* @public
|
|
4
|
+
*/
|
|
5
|
+
export interface IKillerConstraints {
|
|
6
|
+
/**
|
|
7
|
+
* Numbers that cannot be present in the combination.
|
|
8
|
+
* Must be unique values between 1-9.
|
|
9
|
+
*/
|
|
10
|
+
readonly excludedNumbers?: readonly number[];
|
|
11
|
+
/**
|
|
12
|
+
* Numbers that must be present in the combination.
|
|
13
|
+
* Must be unique values between 1-9.
|
|
14
|
+
*/
|
|
15
|
+
readonly requiredNumbers?: readonly number[];
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=killerCombinationsTypes.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* MIT License
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2023 Erik Fortune
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
* copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
* SOFTWARE.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
//# sourceMappingURL=killerCombinationsTypes.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Result } from '@fgv/ts-utils';
|
|
2
|
-
import {
|
|
2
|
+
import { IPuzzleDefinition, Puzzle } from '../common';
|
|
3
3
|
/**
|
|
4
4
|
* @public
|
|
5
5
|
*/
|
|
6
6
|
export declare class KillerSudokuPuzzle extends Puzzle {
|
|
7
7
|
private constructor();
|
|
8
|
-
static create(desc:
|
|
8
|
+
static create(desc: IPuzzleDefinition): Result<Puzzle>;
|
|
9
9
|
private static _getKillerCages;
|
|
10
10
|
private static _getCageCells;
|
|
11
11
|
private static _getCages;
|
|
@@ -53,6 +53,7 @@ class KillerSudokuPuzzle extends common_1.Puzzle {
|
|
|
53
53
|
}
|
|
54
54
|
static _getKillerCages(puzzle) {
|
|
55
55
|
const decl = puzzle.cells.split('|');
|
|
56
|
+
/* c8 ignore next 3 - tested but coverage has intermittent issues */
|
|
56
57
|
if (decl.length !== 2) {
|
|
57
58
|
throw new Error(`malformed cells|cages "${puzzle.cells}"`);
|
|
58
59
|
}
|
|
@@ -66,13 +67,14 @@ class KillerSudokuPuzzle extends common_1.Puzzle {
|
|
|
66
67
|
var _a;
|
|
67
68
|
const cages = new Map();
|
|
68
69
|
const cageMapping = Array.from(mappingDecl);
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
/* c8 ignore next 5 - defensive coding: protected by validation in puzzleDefinitions.ts */
|
|
71
|
+
if (cageMapping.length !== puzzle.totalRows * puzzle.totalColumns) {
|
|
72
|
+
const expected = puzzle.totalRows * puzzle.totalColumns;
|
|
71
73
|
const got = cageMapping.length;
|
|
72
74
|
throw new Error(`expected ${expected} cell mappings, found ${got}`);
|
|
73
75
|
}
|
|
74
|
-
for (let row = 0; row < puzzle.
|
|
75
|
-
for (let col = 0; col < puzzle.
|
|
76
|
+
for (let row = 0; row < puzzle.totalRows; row++) {
|
|
77
|
+
for (let col = 0; col < puzzle.totalColumns; col++) {
|
|
76
78
|
const cage = cageMapping.shift();
|
|
77
79
|
const cell = common_1.Ids.cellId({ row, col }).orThrow();
|
|
78
80
|
const cells = (_a = cages.get(cage)) !== null && _a !== void 0 ? _a : [];
|
|
@@ -82,7 +84,7 @@ class KillerSudokuPuzzle extends common_1.Puzzle {
|
|
|
82
84
|
}
|
|
83
85
|
return cages;
|
|
84
86
|
}
|
|
85
|
-
static _getCages(
|
|
87
|
+
static _getCages(puzzle, cageCells, cagePart) {
|
|
86
88
|
const cages = new Map();
|
|
87
89
|
const cageDefs = cagePart.split(',');
|
|
88
90
|
if (cageDefs.length !== cageCells.size) {
|
|
@@ -100,10 +102,12 @@ class KillerSudokuPuzzle extends common_1.Puzzle {
|
|
|
100
102
|
if (!cells) {
|
|
101
103
|
throw new Error(`cage ${cageId} has no cells`);
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// Use the puzzle's maxValue for dynamic validation
|
|
106
|
+
const maxValue = puzzle.maxValue;
|
|
107
|
+
if (cells.length < 1 || cells.length > maxValue) {
|
|
108
|
+
throw new Error(`invalid cell count ${cells.length} for cage ${cageId} (max ${maxValue})`);
|
|
105
109
|
}
|
|
106
|
-
const { min, max } = common_1.
|
|
110
|
+
const { min, max } = (0, common_1.getCageTotalBounds)(cells.length, maxValue);
|
|
107
111
|
if (total < min || total > max) {
|
|
108
112
|
throw new Error(`invalid total ${total} for cage ${cageId} (expected ${min}..${max})`);
|
|
109
113
|
}
|
|
@@ -113,13 +117,13 @@ class KillerSudokuPuzzle extends common_1.Puzzle {
|
|
|
113
117
|
}
|
|
114
118
|
static _getKillerCells(puzzle, givens) {
|
|
115
119
|
const cells = [];
|
|
116
|
-
for (let row = 0; row < puzzle.
|
|
117
|
-
for (let col = 0; col < puzzle.
|
|
120
|
+
for (let row = 0; row < puzzle.totalRows; row++) {
|
|
121
|
+
for (let col = 0; col < puzzle.totalColumns; col++) {
|
|
118
122
|
const cellId = common_1.Ids.cellId({ row, col }).orThrow();
|
|
119
123
|
const cage = givens.find((g) => g.cellIds[0] === cellId);
|
|
120
124
|
if (cage) {
|
|
121
125
|
/* c8 ignore next 3 - defense in depth should never happen */
|
|
122
|
-
if (cage.total < 1 || cage.total >
|
|
126
|
+
if (cage.total < 1 || cage.total > puzzle.maxValue) {
|
|
123
127
|
throw new Error(`invalid total ${cage.total} for cell ${cellId}`);
|
|
124
128
|
}
|
|
125
129
|
cells.push(String(cage.total));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Result } from '@fgv/ts-utils';
|
|
2
|
-
import {
|
|
2
|
+
import { IPuzzleDefinition, Puzzle } from '../common';
|
|
3
3
|
/**
|
|
4
4
|
* @public
|
|
5
5
|
*/
|
|
6
6
|
export declare class SudokuPuzzle extends Puzzle {
|
|
7
7
|
private constructor();
|
|
8
|
-
static create(puzzle:
|
|
8
|
+
static create(puzzle: IPuzzleDefinition): Result<Puzzle>;
|
|
9
9
|
}
|
|
10
10
|
//# sourceMappingURL=sudokuPuzzle.d.ts.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Result } from '@fgv/ts-utils';
|
|
2
|
-
import {
|
|
2
|
+
import { IPuzzleDefinition, Puzzle } from '../common';
|
|
3
3
|
/**
|
|
4
4
|
* @public
|
|
5
5
|
*/
|
|
6
6
|
export declare class SudokuXPuzzle extends Puzzle {
|
|
7
7
|
private constructor();
|
|
8
|
-
static create(puzzle:
|
|
8
|
+
static create(puzzle: IPuzzleDefinition): Result<Puzzle>;
|
|
9
9
|
private static _getXCages;
|
|
10
10
|
}
|
|
11
11
|
//# sourceMappingURL=sudokuXPuzzle.d.ts.map
|
|
@@ -36,7 +36,11 @@ class SudokuXPuzzle extends common_1.Puzzle {
|
|
|
36
36
|
static create(puzzle) {
|
|
37
37
|
/* c8 ignore next 3 */
|
|
38
38
|
if (puzzle.type !== 'sudoku-x') {
|
|
39
|
-
return fail(`Puzzle '${puzzle.description}' unsupported type ${puzzle.type}`);
|
|
39
|
+
return (0, ts_utils_1.fail)(`Puzzle '${puzzle.description}' unsupported type ${puzzle.type}`);
|
|
40
|
+
}
|
|
41
|
+
// Sudoku X diagonals work for any square puzzle (totalRows === totalColumns)
|
|
42
|
+
if (puzzle.totalRows !== puzzle.totalColumns) {
|
|
43
|
+
return (0, ts_utils_1.fail)(`Sudoku X puzzle must be square, got ${puzzle.totalRows}x${puzzle.totalColumns}`);
|
|
40
44
|
}
|
|
41
45
|
return (0, ts_utils_1.captureResult)(() => {
|
|
42
46
|
return new SudokuXPuzzle(puzzle, SudokuXPuzzle._getXCages(puzzle));
|
|
@@ -47,14 +51,14 @@ class SudokuXPuzzle extends common_1.Puzzle {
|
|
|
47
51
|
static _getXCages(puzzle) {
|
|
48
52
|
const x1Cells = [];
|
|
49
53
|
const x2Cells = [];
|
|
50
|
-
for (let row = 0, col1 = 0, col2 = puzzle.
|
|
54
|
+
for (let row = 0, col1 = 0, col2 = puzzle.totalColumns - 1; row < puzzle.totalRows; row++, col1++, col2--) {
|
|
51
55
|
x1Cells.push(common_1.Ids.cellId({ row, col: col1 }).orThrow());
|
|
52
56
|
x2Cells.push(common_1.Ids.cellId({ row, col: col2 }).orThrow());
|
|
53
57
|
}
|
|
54
58
|
const x1Id = common_1.Ids.cageId('X1').orThrow();
|
|
55
59
|
const x2Id = common_1.Ids.cageId('X2').orThrow();
|
|
56
|
-
const x1 = common_1.Cage.create(x1Id, 'x',
|
|
57
|
-
const x2 = common_1.Cage.create(x2Id, 'x',
|
|
60
|
+
const x1 = common_1.Cage.create(x1Id, 'x', puzzle.basicCageTotal, x1Cells).orThrow();
|
|
61
|
+
const x2 = common_1.Cage.create(x2Id, 'x', puzzle.basicCageTotal, x2Cells).orThrow();
|
|
58
62
|
return [
|
|
59
63
|
[x1Id, x1],
|
|
60
64
|
[x2Id, x2]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fgv/ts-sudoku-lib",
|
|
3
|
-
"version": "5.0.1-
|
|
3
|
+
"version": "5.0.1-2",
|
|
4
4
|
"description": "Sudoku rules library",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "dist/ts-sudoku-lib.d.ts",
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"@rushstack/eslint-config": "4.4.0",
|
|
44
44
|
"@rushstack/heft-jest-plugin": "0.16.13",
|
|
45
45
|
"eslint-plugin-tsdoc": "~0.4.0",
|
|
46
|
-
"@fgv/ts-utils": "5.0.1-
|
|
47
|
-
"@fgv/ts-
|
|
48
|
-
"@fgv/ts-
|
|
46
|
+
"@fgv/ts-utils": "5.0.1-2",
|
|
47
|
+
"@fgv/ts-json-base": "5.0.1-2",
|
|
48
|
+
"@fgv/ts-utils-jest": "5.0.1-2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@fgv/ts-utils": "5.0.1-
|
|
51
|
+
"@fgv/ts-utils": "5.0.1-2"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "heft build --clean",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Sample 12x12 puzzle generated from screenshot
|
|
4
|
+
const { PuzzleDefinitionFactory, STANDARD_CONFIGS } = require('./lib/packlets/common');
|
|
5
|
+
|
|
6
|
+
console.log('🧩 Sample 12x12 Sudoku Puzzle\n');
|
|
7
|
+
|
|
8
|
+
// 12x12 puzzle from screenshot
|
|
9
|
+
// Note: A=10, B=11, C=12 in standard 12x12 Sudoku
|
|
10
|
+
// Rebuilding with exact 12 chars per row (total 144):
|
|
11
|
+
// Row 0: ..1.6.2.A7.. (12 chars)
|
|
12
|
+
// Row 1: 62.5B....1.. (12 chars)
|
|
13
|
+
// Row 2: .8....C3B.62 (12 chars)
|
|
14
|
+
// Row 3: A97.2.6..B5. (12 chars)
|
|
15
|
+
// Row 4: .5......37C. (12 chars) - was missing final .
|
|
16
|
+
// Row 5: 3B....7...9. (12 chars) - was missing final .
|
|
17
|
+
// Row 6: 57B.4.....1. (12 chars) - was missing final .
|
|
18
|
+
// Row 7: 8.....A.9... (12 chars) - was missing final .
|
|
19
|
+
// Row 8: .4..97...6.. (12 chars)
|
|
20
|
+
// Row 9: .1C.......B. (12 chars) - was missing final .
|
|
21
|
+
// Row 10: 4....2.C.... (12 chars) - was missing final .
|
|
22
|
+
// Row 11: ....5C..7126 (12 chars)
|
|
23
|
+
|
|
24
|
+
const puzzle12x12Description = {
|
|
25
|
+
description: 'Sample 12x12 Sudoku #1',
|
|
26
|
+
type: 'sudoku',
|
|
27
|
+
level: 3,
|
|
28
|
+
cells:
|
|
29
|
+
'..1.6.2.A7..62.5B....1...8....C3B.62A97.2.6..B5..5......37C.3B....7...9.57B.4.....1.8.....A.9....4..97...6...1C.......B.4....2.C........5C..7126'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Create puzzle definition
|
|
33
|
+
console.log('Creating 12x12 puzzle definition...\n');
|
|
34
|
+
|
|
35
|
+
const puzzleResult = PuzzleDefinitionFactory.create(STANDARD_CONFIGS.puzzle12x12, puzzle12x12Description);
|
|
36
|
+
|
|
37
|
+
if (puzzleResult.isSuccess()) {
|
|
38
|
+
const puzzle = puzzleResult.value;
|
|
39
|
+
|
|
40
|
+
console.log('✅ 12x12 Puzzle:');
|
|
41
|
+
console.log(` Description: ${puzzle.description}`);
|
|
42
|
+
console.log(` Grid: ${puzzle.totalRows}x${puzzle.totalColumns}`);
|
|
43
|
+
console.log(` Cage dimensions: ${puzzle.cageWidthInCells}x${puzzle.cageHeightInCells} (4 wide x 3 high)`);
|
|
44
|
+
console.log(` Board dimensions: ${puzzle.boardWidthInCages}x${puzzle.boardHeightInCages} cages`);
|
|
45
|
+
console.log(` Max value: ${puzzle.maxValue}`);
|
|
46
|
+
console.log(` Cage total: ${puzzle.basicCageTotal} (sum 1+2+...+12 = 78)`);
|
|
47
|
+
console.log(` Cells: ${puzzle.cells}`);
|
|
48
|
+
console.log('\n Grid layout:');
|
|
49
|
+
|
|
50
|
+
for (let row = 0; row < 12; row++) {
|
|
51
|
+
const rowCells = puzzle.cells.slice(row * 12, (row + 1) * 12).split('');
|
|
52
|
+
const displayRow = rowCells.map((c) => (c === '.' ? ' ' : c)).join(' ');
|
|
53
|
+
console.log(` ${displayRow}`);
|
|
54
|
+
|
|
55
|
+
// Show cage boundaries after rows 2, 5, 8
|
|
56
|
+
if (row === 2 || row === 5 || row === 8) {
|
|
57
|
+
console.log(' -----------------------------------------------'); // Horizontal cage separator
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log('\n Cage structure (4x3 sections):');
|
|
62
|
+
console.log(' Each cage is 4 cells wide × 3 cells high');
|
|
63
|
+
console.log(' Grid has 3×4 = 12 total cages');
|
|
64
|
+
console.log(' Values: 1,2,3,4,5,6,7,8,9,A(10),B(11),C(12)');
|
|
65
|
+
|
|
66
|
+
console.log('\n🎯 12x12 puzzle definition created successfully!');
|
|
67
|
+
console.log('This puzzle uses 4x3 cage dimensions with 3x4 cage layout.');
|
|
68
|
+
console.log('Each cage must contain all values 1-12 (represented as 1-9,A,B,C).');
|
|
69
|
+
} else {
|
|
70
|
+
console.error('❌ Failed to create 12x12 puzzle definition:');
|
|
71
|
+
console.error(` Error: ${puzzleResult.message}`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Sample 4x4 puzzles generated from screenshot
|
|
4
|
+
const { PuzzleDefinitionFactory, STANDARD_CONFIGS } = require('./lib/packlets/common');
|
|
5
|
+
|
|
6
|
+
console.log('🧩 Sample 4x4 Sudoku Puzzles\n');
|
|
7
|
+
|
|
8
|
+
// First 4x4 puzzle from screenshot
|
|
9
|
+
const puzzle1Description = {
|
|
10
|
+
description: 'Sample 4x4 Sudoku #1',
|
|
11
|
+
type: 'sudoku',
|
|
12
|
+
level: 1,
|
|
13
|
+
cells: '..1....2.2....4.'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Second 4x4 puzzle from screenshot
|
|
17
|
+
const puzzle2Description = {
|
|
18
|
+
description: 'Sample 4x4 Sudoku #2',
|
|
19
|
+
type: 'sudoku',
|
|
20
|
+
level: 1,
|
|
21
|
+
cells: '3..1..2.4.......'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Create puzzle definitions
|
|
25
|
+
console.log('Creating puzzle definitions...\n');
|
|
26
|
+
|
|
27
|
+
const puzzle1Result = PuzzleDefinitionFactory.create(STANDARD_CONFIGS.puzzle4x4, puzzle1Description);
|
|
28
|
+
const puzzle2Result = PuzzleDefinitionFactory.create(STANDARD_CONFIGS.puzzle4x4, puzzle2Description);
|
|
29
|
+
|
|
30
|
+
if (puzzle1Result.isSuccess() && puzzle2Result.isSuccess()) {
|
|
31
|
+
const puzzle1 = puzzle1Result.value;
|
|
32
|
+
const puzzle2 = puzzle2Result.value;
|
|
33
|
+
|
|
34
|
+
console.log('✅ Puzzle #1:');
|
|
35
|
+
console.log(` Description: ${puzzle1.description}`);
|
|
36
|
+
console.log(` Grid: ${puzzle1.totalRows}x${puzzle1.totalColumns}`);
|
|
37
|
+
console.log(` Cage dimensions: ${puzzle1.cageWidthInCells}x${puzzle1.cageHeightInCells}`);
|
|
38
|
+
console.log(` Max value: ${puzzle1.maxValue}`);
|
|
39
|
+
console.log(` Cells: ${puzzle1.cells}`);
|
|
40
|
+
console.log(' Grid layout:');
|
|
41
|
+
for (let row = 0; row < 4; row++) {
|
|
42
|
+
const rowCells = puzzle1.cells.slice(row * 4, (row + 1) * 4).split('');
|
|
43
|
+
console.log(` ${rowCells.map((c) => (c === '.' ? ' ' : c)).join(' ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('\n✅ Puzzle #2:');
|
|
47
|
+
console.log(` Description: ${puzzle2.description}`);
|
|
48
|
+
console.log(` Grid: ${puzzle2.totalRows}x${puzzle2.totalColumns}`);
|
|
49
|
+
console.log(` Cage dimensions: ${puzzle2.cageWidthInCells}x${puzzle2.cageHeightInCells}`);
|
|
50
|
+
console.log(` Max value: ${puzzle2.maxValue}`);
|
|
51
|
+
console.log(` Cells: ${puzzle2.cells}`);
|
|
52
|
+
console.log(' Grid layout:');
|
|
53
|
+
for (let row = 0; row < 4; row++) {
|
|
54
|
+
const rowCells = puzzle2.cells.slice(row * 4, (row + 1) * 4).split('');
|
|
55
|
+
console.log(` ${rowCells.map((c) => (c === '.' ? ' ' : c)).join(' ')}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('\n🎯 Both 4x4 puzzle definitions created successfully!');
|
|
59
|
+
console.log('These can be used with the Sudoku puzzle engine.');
|
|
60
|
+
} else {
|
|
61
|
+
console.error('❌ Failed to create puzzle definitions:');
|
|
62
|
+
if (puzzle1Result.isFailure()) {
|
|
63
|
+
console.error(` Puzzle #1: ${puzzle1Result.message}`);
|
|
64
|
+
}
|
|
65
|
+
if (puzzle2Result.isFailure()) {
|
|
66
|
+
console.error(` Puzzle #2: ${puzzle2Result.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Sample 6x6 puzzle generated from screenshot
|
|
4
|
+
const { PuzzleDefinitionFactory, STANDARD_CONFIGS } = require('./lib/packlets/common');
|
|
5
|
+
|
|
6
|
+
console.log('🧩 Sample 6x6 Sudoku Puzzle\n');
|
|
7
|
+
|
|
8
|
+
// 6x6 puzzle from screenshot
|
|
9
|
+
// Row by row: 62..5.3, ......, 5....3, ..6..2, ....346, 3.6...
|
|
10
|
+
const puzzle6x6Description = {
|
|
11
|
+
description: 'Sample 6x6 Sudoku #1',
|
|
12
|
+
type: 'sudoku',
|
|
13
|
+
level: 2,
|
|
14
|
+
cells: '62..533....5....3..6..2....3463.6...'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Create puzzle definition
|
|
18
|
+
console.log('Creating 6x6 puzzle definition...\n');
|
|
19
|
+
|
|
20
|
+
const puzzleResult = PuzzleDefinitionFactory.create(STANDARD_CONFIGS.puzzle6x6, puzzle6x6Description);
|
|
21
|
+
|
|
22
|
+
if (puzzleResult.isSuccess()) {
|
|
23
|
+
const puzzle = puzzleResult.value;
|
|
24
|
+
|
|
25
|
+
console.log('✅ 6x6 Puzzle:');
|
|
26
|
+
console.log(` Description: ${puzzle.description}`);
|
|
27
|
+
console.log(` Grid: ${puzzle.totalRows}x${puzzle.totalColumns}`);
|
|
28
|
+
console.log(` Cage dimensions: ${puzzle.cageWidthInCells}x${puzzle.cageHeightInCells} (3 wide x 2 high)`);
|
|
29
|
+
console.log(` Board dimensions: ${puzzle.boardWidthInCages}x${puzzle.boardHeightInCages} cages`);
|
|
30
|
+
console.log(` Max value: ${puzzle.maxValue}`);
|
|
31
|
+
console.log(` Cage total: ${puzzle.basicCageTotal} (sum 1+2+3+4+5+6)`);
|
|
32
|
+
console.log(` Cells: ${puzzle.cells}`);
|
|
33
|
+
console.log(' Grid layout:');
|
|
34
|
+
|
|
35
|
+
for (let row = 0; row < 6; row++) {
|
|
36
|
+
const rowCells = puzzle.cells.slice(row * 6, (row + 1) * 6).split('');
|
|
37
|
+
const displayRow = rowCells.map((c) => (c === '.' ? ' ' : c)).join(' ');
|
|
38
|
+
console.log(` ${displayRow}`);
|
|
39
|
+
|
|
40
|
+
// Show cage boundaries after rows 1, 3, 5
|
|
41
|
+
if (row === 1 || row === 3) {
|
|
42
|
+
console.log(' -----------'); // Horizontal cage separator
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('\n Cage structure (3x2 sections):');
|
|
47
|
+
console.log(' Top left | Top right');
|
|
48
|
+
console.log(' (rows 0-1) | (rows 0-1)');
|
|
49
|
+
console.log(' -----------');
|
|
50
|
+
console.log(' Mid left | Mid right');
|
|
51
|
+
console.log(' (rows 2-3) | (rows 2-3)');
|
|
52
|
+
console.log(' -----------');
|
|
53
|
+
console.log(' Bot left | Bot right');
|
|
54
|
+
console.log(' (rows 4-5) | (rows 4-5)');
|
|
55
|
+
|
|
56
|
+
console.log('\n🎯 6x6 puzzle definition created successfully!');
|
|
57
|
+
console.log('This puzzle uses 3x2 cage dimensions with 2x3 cage layout.');
|
|
58
|
+
} else {
|
|
59
|
+
console.error('❌ Failed to create 6x6 puzzle definition:');
|
|
60
|
+
console.error(` Error: ${puzzleResult.message}`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Row 0: ..1.6.2.A7..
|
|
2
|
+
Row 1: 62.5B....1..
|
|
3
|
+
Row 2: .8....C3B.62
|
|
4
|
+
Row 3: A97.2.6..B5.
|
|
5
|
+
Row 4: .5......37C
|
|
6
|
+
Row 5: 3B....7...9
|
|
7
|
+
Row 6: 57B.4.....1
|
|
8
|
+
Row 7: 8.....A.9..
|
|
9
|
+
Row 8: .4..97...6..
|
|
10
|
+
Row 9: .1C.......B
|
|
11
|
+
Row 10: 4....2.C...
|
|
12
|
+
Row 11: ....5C..7126
|