@fgv/ts-sudoku-lib 5.0.1-0 → 5.0.1-10
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/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 +1515 -47
- package/dist/tsdoc-metadata.json +1 -1
- package/lib/index.browser.d.ts +7 -0
- package/lib/index.browser.js +78 -0
- 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/files/fileTreeHelpers.d.ts +13 -0
- package/lib/packlets/files/fileTreeHelpers.js +40 -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/{file/index.d.ts → files/index.browser.d.ts} +2 -1
- package/lib/packlets/{file/index.js → files/index.browser.js} +9 -3
- package/lib/packlets/files/index.d.ts +6 -0
- package/lib/packlets/files/index.js +69 -0
- 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 +32 -18
- 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/model.d.ts +0 -9
- /package/lib/packlets/{common → files}/model.js +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
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, mapResults, succeed } from '@fgv/ts-utils';
|
|
25
|
+
import { Cage } from './cage';
|
|
26
|
+
import { Cell } from './cell';
|
|
27
|
+
import { Ids } from './ids';
|
|
28
|
+
import { PuzzleState } from './puzzleState';
|
|
29
|
+
/**
|
|
30
|
+
* Abstract base class for all puzzles.
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export class Puzzle {
|
|
34
|
+
/**
|
|
35
|
+
* Constructs a new puzzle state.
|
|
36
|
+
* @param puzzle - {@Link IPuzzleDefinition | Puzzle definition} from which this puzzle state
|
|
37
|
+
* is to be initialized.
|
|
38
|
+
*/
|
|
39
|
+
constructor(puzzle, extraCages) {
|
|
40
|
+
/* c8 ignore next - ?? is defense in depth */
|
|
41
|
+
extraCages = extraCages !== null && extraCages !== void 0 ? extraCages : [];
|
|
42
|
+
// Validate cell count matches grid size
|
|
43
|
+
/* c8 ignore next 7 - defensive test - not reachable in practice */
|
|
44
|
+
if (puzzle.cells.length !== puzzle.totalRows * puzzle.totalColumns) {
|
|
45
|
+
throw new Error(`Puzzle '${puzzle.description}" expected ${puzzle.totalRows * puzzle.totalColumns} cells, found ${puzzle.cells.length}`);
|
|
46
|
+
}
|
|
47
|
+
this.id = puzzle.id;
|
|
48
|
+
this.description = puzzle.description;
|
|
49
|
+
this.type = puzzle.type;
|
|
50
|
+
this.dimensions = puzzle;
|
|
51
|
+
const rows = Puzzle._createRowCages(puzzle.totalRows, puzzle.totalColumns, puzzle.basicCageTotal).orThrow();
|
|
52
|
+
const columns = Puzzle._createColumnCages(puzzle.totalRows, puzzle.totalColumns, puzzle.basicCageTotal).orThrow();
|
|
53
|
+
const sections = Puzzle._createSectionCages(puzzle).orThrow();
|
|
54
|
+
const cages = [...rows, ...columns, ...sections, ...extraCages];
|
|
55
|
+
this._rows = new Map(rows);
|
|
56
|
+
this._columns = new Map(columns);
|
|
57
|
+
this._sections = new Map(sections);
|
|
58
|
+
this._cages = new Map(cages);
|
|
59
|
+
this._cells = new Map();
|
|
60
|
+
const cellInit = [...puzzle.cells];
|
|
61
|
+
for (let row = 0; row < this._rows.size; row++) {
|
|
62
|
+
const rowCage = this.getRow(row).orThrow();
|
|
63
|
+
for (let col = 0; col < this._columns.size; col++) {
|
|
64
|
+
const id = Ids.cellId({ row, col }).orThrow();
|
|
65
|
+
const colCage = this.getColumn(col).orThrow();
|
|
66
|
+
const sectionCage = this.getSection({ row, col }).orThrow();
|
|
67
|
+
const otherCages = extraCages.filter(([__key, cage]) => cage.containsCell(id));
|
|
68
|
+
const init = cellInit.shift();
|
|
69
|
+
/* c8 ignore next - defense in depth/make type check happy. undefined init should never happen */
|
|
70
|
+
const immutableValue = init === '.' ? undefined : this._parseAlphanumericValue(init !== null && init !== void 0 ? init : '0');
|
|
71
|
+
if (immutableValue !== undefined &&
|
|
72
|
+
(Number.isNaN(immutableValue) || immutableValue < 1 || immutableValue > puzzle.maxValue)) {
|
|
73
|
+
throw new Error(`Puzzle ${puzzle.description} illegal value "${init}" for cell ${id}`);
|
|
74
|
+
}
|
|
75
|
+
const cages = [rowCage, colCage, sectionCage, ...otherCages.map(([__key, state]) => state)];
|
|
76
|
+
const cell = new Cell({ id, immutableValue, row, col }, cages);
|
|
77
|
+
this._cells.set(id, cell);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.initialState = PuzzleState.create(Array.from(this._cells.entries()).map(([id, state]) => {
|
|
81
|
+
return { id, value: state.immutableValue, notes: [] };
|
|
82
|
+
})).orThrow();
|
|
83
|
+
}
|
|
84
|
+
get numRows() {
|
|
85
|
+
return this._rows.size;
|
|
86
|
+
}
|
|
87
|
+
get numColumns() {
|
|
88
|
+
return this._columns.size;
|
|
89
|
+
}
|
|
90
|
+
get rows() {
|
|
91
|
+
return Array.from(this._rows.values());
|
|
92
|
+
}
|
|
93
|
+
get cols() {
|
|
94
|
+
return Array.from(this._columns.values());
|
|
95
|
+
}
|
|
96
|
+
get sections() {
|
|
97
|
+
return Array.from(this._sections.values());
|
|
98
|
+
}
|
|
99
|
+
get cages() {
|
|
100
|
+
return Array.from(this._cages.values());
|
|
101
|
+
}
|
|
102
|
+
get cells() {
|
|
103
|
+
return Array.from(this._cells.values());
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
static _createRowCages(numRows, numCols, basicCageTotal) {
|
|
109
|
+
const cages = [];
|
|
110
|
+
for (let r = 0; r < numRows; r++) {
|
|
111
|
+
const id = Ids.rowCageId(r);
|
|
112
|
+
const cellIds = Ids.cellIds(r, 1, 0, numCols);
|
|
113
|
+
/* c8 ignore next 3 - defense in depth should never happen */
|
|
114
|
+
if (cellIds.isFailure()) {
|
|
115
|
+
return fail(cellIds.message);
|
|
116
|
+
}
|
|
117
|
+
const result = Cage.create(id, 'row', basicCageTotal, cellIds.value).onSuccess((cage) => {
|
|
118
|
+
cages.push([id, cage]);
|
|
119
|
+
return succeed(cage);
|
|
120
|
+
});
|
|
121
|
+
/* c8 ignore next 3 - defense in depth should never happen */
|
|
122
|
+
if (result.isFailure()) {
|
|
123
|
+
return fail(result.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return succeed(cages);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
static _createColumnCages(numRows, numCols, basicCageTotal) {
|
|
132
|
+
const cages = [];
|
|
133
|
+
for (let c = 0; c < numCols; c++) {
|
|
134
|
+
const id = Ids.columnCageId(c);
|
|
135
|
+
const cellIds = Ids.cellIds(0, numRows, c, 1);
|
|
136
|
+
/* c8 ignore next 3 - defense in depth should never happen */
|
|
137
|
+
if (cellIds.isFailure()) {
|
|
138
|
+
return fail(cellIds.message);
|
|
139
|
+
}
|
|
140
|
+
const result = Cage.create(id, 'column', basicCageTotal, cellIds.value).onSuccess((cage) => {
|
|
141
|
+
cages.push([id, cage]);
|
|
142
|
+
return succeed(cage);
|
|
143
|
+
});
|
|
144
|
+
/* c8 ignore next 3 - defense in depth should never happen */
|
|
145
|
+
if (result.isFailure()) {
|
|
146
|
+
return fail(result.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return succeed(cages);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Parse alphanumeric cell value (1-9, A-Z for values 10-35)
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
_parseAlphanumericValue(char) {
|
|
156
|
+
// Handle digits 1-9
|
|
157
|
+
if (char >= '1' && char <= '9') {
|
|
158
|
+
return Number.parseInt(char, 10);
|
|
159
|
+
}
|
|
160
|
+
// Handle uppercase A-Z for values 10-35
|
|
161
|
+
if (char >= 'A' && char <= 'Z') {
|
|
162
|
+
return char.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
|
|
163
|
+
}
|
|
164
|
+
// Handle lowercase a-z for values 10-35
|
|
165
|
+
if (char >= 'a' && char <= 'z') {
|
|
166
|
+
return char.charCodeAt(0) - 'a'.charCodeAt(0) + 10;
|
|
167
|
+
}
|
|
168
|
+
// Invalid character, return NaN to trigger error handling
|
|
169
|
+
return Number.NaN;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
static _createSectionCages(puzzle) {
|
|
175
|
+
const cages = [];
|
|
176
|
+
const cageWidth = puzzle.cageWidthInCells;
|
|
177
|
+
const cageHeight = puzzle.cageHeightInCells;
|
|
178
|
+
const boardWidth = puzzle.boardWidthInCages;
|
|
179
|
+
const boardHeight = puzzle.boardHeightInCages;
|
|
180
|
+
for (let boardRow = 0; boardRow < boardHeight; boardRow++) {
|
|
181
|
+
for (let boardCol = 0; boardCol < boardWidth; boardCol++) {
|
|
182
|
+
const startRow = boardRow * cageHeight;
|
|
183
|
+
const startCol = boardCol * cageWidth;
|
|
184
|
+
const id = Ids.sectionCageId(startRow, startCol, cageHeight, cageWidth);
|
|
185
|
+
const cellIds = Ids.cellIds(startRow, cageHeight, startCol, cageWidth);
|
|
186
|
+
/* c8 ignore next 3 - defense in depth should never happen */
|
|
187
|
+
if (cellIds.isFailure()) {
|
|
188
|
+
return fail(cellIds.message);
|
|
189
|
+
}
|
|
190
|
+
const result = Cage.create(id, 'section', puzzle.basicCageTotal, cellIds.value).onSuccess((cage) => {
|
|
191
|
+
cages.push([id, cage]);
|
|
192
|
+
return succeed(cage);
|
|
193
|
+
});
|
|
194
|
+
/* c8 ignore next 3 - defense in depth should never happen */
|
|
195
|
+
if (result.isFailure()) {
|
|
196
|
+
return fail(result.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return succeed(cages);
|
|
201
|
+
}
|
|
202
|
+
checkIsSolved(state) {
|
|
203
|
+
for (const id of this._cells.keys()) {
|
|
204
|
+
if (!state.hasValue(id)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const cell of this._cells.values()) {
|
|
209
|
+
if (!cell.isValid(state)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
checkIsValid(state) {
|
|
216
|
+
for (const cell of this._cells.values()) {
|
|
217
|
+
if (!cell.isValid(state)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
getEmptyCells(state) {
|
|
224
|
+
return Array.from(this._cells.values()).filter((c) => !state.hasValue(c.id));
|
|
225
|
+
}
|
|
226
|
+
getInvalidCells(state) {
|
|
227
|
+
return Array.from(this._cells.values()).filter((c) => !c.isValid(state));
|
|
228
|
+
}
|
|
229
|
+
getCellContents(spec, state) {
|
|
230
|
+
return this.getCell(spec).onSuccess((cell) => {
|
|
231
|
+
if (cell.immutable) {
|
|
232
|
+
const contents = { value: cell.immutableValue, notes: [] };
|
|
233
|
+
return succeed({ cell, contents });
|
|
234
|
+
}
|
|
235
|
+
return state.getCellContents(cell.id).onSuccess((contents) => {
|
|
236
|
+
return succeed({ cell, contents });
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
getCell(spec) {
|
|
241
|
+
const want = Ids.cellId(spec);
|
|
242
|
+
if (want.isFailure()) {
|
|
243
|
+
return fail(want.message);
|
|
244
|
+
}
|
|
245
|
+
const cell = this._cells.get(want.value);
|
|
246
|
+
return cell ? succeed(cell) : fail(`Cell ${want.value} not found`);
|
|
247
|
+
}
|
|
248
|
+
getCellNeighbor(spec, direction, wrap) {
|
|
249
|
+
return this.getCell(spec).onSuccess((cell) => {
|
|
250
|
+
const move = direction === 'left' || direction === 'right'
|
|
251
|
+
? this._moveColumn(cell, direction, wrap)
|
|
252
|
+
: this._moveRow(cell, direction, wrap);
|
|
253
|
+
return move.onSuccess((next) => this.getCell(next));
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
updateContents(wantUpdates, state) {
|
|
257
|
+
wantUpdates = Array.isArray(wantUpdates) ? wantUpdates : [wantUpdates];
|
|
258
|
+
return mapResults(wantUpdates.map((u) => this.getCellContents(u.id, state).onSuccess((existing) => existing.cell.update(u.value, u.notes).onSuccess((to) => {
|
|
259
|
+
const from = Object.assign({ id: u.id }, existing.contents);
|
|
260
|
+
return succeed({ from, to });
|
|
261
|
+
})))).onSuccess((cellUpdates) => {
|
|
262
|
+
return state.update(cellUpdates.map(({ to }) => to)).onSuccess((updatedState) => {
|
|
263
|
+
return succeed({
|
|
264
|
+
from: state,
|
|
265
|
+
to: updatedState,
|
|
266
|
+
cells: cellUpdates
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
updateValues(wantUpdates, state) {
|
|
272
|
+
wantUpdates = Array.isArray(wantUpdates) ? wantUpdates : [wantUpdates];
|
|
273
|
+
return mapResults(wantUpdates.map((u) => this.getCellContents(u.id, state).onSuccess((existing) => existing.cell.update(u.value, existing.contents.notes).onSuccess((to) => {
|
|
274
|
+
const from = Object.assign({ id: u.id }, existing.contents);
|
|
275
|
+
return succeed({ from, to });
|
|
276
|
+
})))).onSuccess((cellUpdates) => {
|
|
277
|
+
return state.update(cellUpdates.map(({ to }) => to)).onSuccess((updatedState) => {
|
|
278
|
+
return succeed({
|
|
279
|
+
from: state,
|
|
280
|
+
to: updatedState,
|
|
281
|
+
cells: cellUpdates
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
updateNotes(wantUpdates, state) {
|
|
287
|
+
wantUpdates = Array.isArray(wantUpdates) ? wantUpdates : [wantUpdates];
|
|
288
|
+
return mapResults(wantUpdates.map((u) => this.getCellContents(u.id, state).onSuccess((existing) => existing.cell.update(existing.contents.value, u.notes).onSuccess((to) => {
|
|
289
|
+
const from = Object.assign({ id: u.id }, existing.contents);
|
|
290
|
+
return succeed({ from, to });
|
|
291
|
+
})))).onSuccess((cellUpdates) => {
|
|
292
|
+
return state.update(cellUpdates.map(({ to }) => to)).onSuccess((updatedState) => {
|
|
293
|
+
return succeed({
|
|
294
|
+
from: state,
|
|
295
|
+
to: updatedState,
|
|
296
|
+
cells: cellUpdates
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
updateCellValue(want, value, state) {
|
|
302
|
+
const idResult = Ids.cellId(want);
|
|
303
|
+
const notes = [];
|
|
304
|
+
return idResult.onSuccess((id) => {
|
|
305
|
+
const update = { id, value, notes };
|
|
306
|
+
return this.updateValues([update], state);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
updateCellNotes(want, notes, state) {
|
|
310
|
+
const idResult = Ids.cellId(want);
|
|
311
|
+
const value = undefined;
|
|
312
|
+
return idResult.onSuccess((id) => {
|
|
313
|
+
const update = { id, value, notes };
|
|
314
|
+
return this.updateNotes([update], state);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
getRow(row) {
|
|
318
|
+
const id = typeof row === 'number' ? Ids.rowCageId(row) : row;
|
|
319
|
+
const cage = this._rows.get(id);
|
|
320
|
+
return cage ? succeed(cage) : fail(`Row ${id} not found`);
|
|
321
|
+
}
|
|
322
|
+
getColumn(col) {
|
|
323
|
+
const id = typeof col === 'number' ? Ids.columnCageId(col) : col;
|
|
324
|
+
const cage = this._columns.get(id);
|
|
325
|
+
return cage ? succeed(cage) : fail(`Column ${id} not found`);
|
|
326
|
+
}
|
|
327
|
+
getSection(spec) {
|
|
328
|
+
const id = typeof spec === 'object'
|
|
329
|
+
? Ids.sectionCageId(spec.row, spec.col, this.dimensions.cageHeightInCells, this.dimensions.cageWidthInCells)
|
|
330
|
+
: spec;
|
|
331
|
+
const cage = this._sections.get(id);
|
|
332
|
+
return cage ? succeed(cage) : fail(`Section ${id} not found`);
|
|
333
|
+
}
|
|
334
|
+
getCage(id) {
|
|
335
|
+
const cage = this._cages.get(id);
|
|
336
|
+
return cage ? succeed(cage) : fail(`Cage ${id} not found`);
|
|
337
|
+
}
|
|
338
|
+
toStrings(state) {
|
|
339
|
+
return Array.from(this._rows.values()).map((row) => row.toString(state));
|
|
340
|
+
}
|
|
341
|
+
toString(state) {
|
|
342
|
+
return this.toStrings(state).join('\n');
|
|
343
|
+
}
|
|
344
|
+
_moveColumn(current, direction, wrap) {
|
|
345
|
+
const row = current.row;
|
|
346
|
+
let col = current.col;
|
|
347
|
+
if (direction === 'left') {
|
|
348
|
+
if (col > 0) {
|
|
349
|
+
col = col - 1;
|
|
350
|
+
}
|
|
351
|
+
else if (wrap !== 'none') {
|
|
352
|
+
col = this.numColumns - 1;
|
|
353
|
+
if (wrap === 'wrap-next') {
|
|
354
|
+
return this._moveRow({ row, col }, 'up', 'wrap-around');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
return fail(`cannot move left from column ${current.col}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else if (direction === 'right') {
|
|
362
|
+
if (col < this.numColumns - 1) {
|
|
363
|
+
col = col + 1;
|
|
364
|
+
}
|
|
365
|
+
else if (wrap !== 'none') {
|
|
366
|
+
col = 0;
|
|
367
|
+
if (wrap === 'wrap-next') {
|
|
368
|
+
return this._moveRow({ row, col }, 'down', 'wrap-around');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
return fail(`cannot move right from column ${current.col}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return succeed({ row, col });
|
|
376
|
+
}
|
|
377
|
+
_moveRow(current, direction, wrap) {
|
|
378
|
+
let row = current.row;
|
|
379
|
+
const col = current.col;
|
|
380
|
+
if (direction === 'up') {
|
|
381
|
+
if (row > 0) {
|
|
382
|
+
row = row - 1;
|
|
383
|
+
}
|
|
384
|
+
else if (wrap !== 'none') {
|
|
385
|
+
row = this.numRows - 1;
|
|
386
|
+
if (wrap === 'wrap-next') {
|
|
387
|
+
return this._moveColumn({ row, col }, 'left', 'wrap-around');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
return fail(`cannot move up from row ${current.row}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else if (direction === 'down') {
|
|
395
|
+
if (row < this.numRows - 1) {
|
|
396
|
+
row = row + 1;
|
|
397
|
+
}
|
|
398
|
+
else if (wrap !== 'none') {
|
|
399
|
+
row = 0;
|
|
400
|
+
if (wrap === 'wrap-next') {
|
|
401
|
+
return this._moveColumn({ row, col }, 'right', 'wrap-around');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
return fail(`cannot move down from row ${current.row}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return succeed({ row, col });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=puzzle.js.map
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MIT License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2023 Erik Fortune
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
import { succeed, fail, captureResult } from '@fgv/ts-utils';
|
|
25
|
+
/**
|
|
26
|
+
* Default validator for standard Sudoku puzzles
|
|
27
|
+
*/
|
|
28
|
+
class StandardSudokuValidator {
|
|
29
|
+
validateCells(cells, dimensions) {
|
|
30
|
+
const expectedLength = dimensions.cageHeightInCells *
|
|
31
|
+
dimensions.boardHeightInCages *
|
|
32
|
+
dimensions.cageWidthInCells *
|
|
33
|
+
dimensions.boardWidthInCages;
|
|
34
|
+
if (cells.length !== expectedLength) {
|
|
35
|
+
return fail(`Expected ${expectedLength} cells, got ${cells.length}`);
|
|
36
|
+
}
|
|
37
|
+
return succeed(true);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validator for Killer Sudoku puzzles - understands extended format with cage definitions
|
|
42
|
+
*/
|
|
43
|
+
class KillerSudokuValidator {
|
|
44
|
+
validateCells(cells, dimensions) {
|
|
45
|
+
const expectedBasicLength = dimensions.cageHeightInCells *
|
|
46
|
+
dimensions.boardHeightInCages *
|
|
47
|
+
dimensions.cageWidthInCells *
|
|
48
|
+
dimensions.boardWidthInCages;
|
|
49
|
+
// Killer sudoku cells format includes cage definitions, so it will be longer than basic format
|
|
50
|
+
// The basic grid should be at the beginning of the string
|
|
51
|
+
if (cells.length < expectedBasicLength) {
|
|
52
|
+
return fail(`Killer sudoku cells must contain at least ${expectedBasicLength} grid cells, got ${cells.length}`);
|
|
53
|
+
}
|
|
54
|
+
// Find the separator between grid and cage definitions
|
|
55
|
+
const separatorIndex = cells.indexOf('|');
|
|
56
|
+
if (separatorIndex === -1) {
|
|
57
|
+
return fail('Killer sudoku cells must contain cage definitions after "|" separator');
|
|
58
|
+
}
|
|
59
|
+
if (separatorIndex !== expectedBasicLength) {
|
|
60
|
+
return fail(`Killer sudoku grid portion must be exactly ${expectedBasicLength} characters, got ${separatorIndex}`);
|
|
61
|
+
}
|
|
62
|
+
// Extract the basic grid portion (before the | separator)
|
|
63
|
+
const gridPortion = cells.substring(0, separatorIndex);
|
|
64
|
+
// For killer sudoku, the grid portion contains:
|
|
65
|
+
// - Digits 1-9 for prefilled values
|
|
66
|
+
// - '.' for empty cells
|
|
67
|
+
// - Letters A-Z, a-z for cage identifiers
|
|
68
|
+
const maxDigit = dimensions.cageWidthInCells * dimensions.cageHeightInCells;
|
|
69
|
+
const validDigits = new Set(['.', ...Array.from({ length: maxDigit }, (__val, i) => String(i + 1))]);
|
|
70
|
+
const validCageLetters = new Set([...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', ...'abcdefghijklmnopqrstuvwxyz']);
|
|
71
|
+
for (let i = 0; i < gridPortion.length; i++) {
|
|
72
|
+
const char = gridPortion[i];
|
|
73
|
+
if (!validDigits.has(char) && !validCageLetters.has(char)) {
|
|
74
|
+
return fail(`Invalid character '${char}' at position ${i} in killer sudoku grid portion`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return succeed(true);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validator for Sudoku X puzzles - same as standard but with diagonal constraints
|
|
82
|
+
*/
|
|
83
|
+
class SudokuXValidator {
|
|
84
|
+
validateCells(cells, dimensions) {
|
|
85
|
+
// Sudoku X uses the same cell format as standard sudoku
|
|
86
|
+
return new StandardSudokuValidator().validateCells(cells, dimensions);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Registry of validators for different puzzle types
|
|
91
|
+
*/
|
|
92
|
+
const PUZZLE_TYPE_VALIDATORS = {
|
|
93
|
+
sudoku: new StandardSudokuValidator(),
|
|
94
|
+
'killer-sudoku': new KillerSudokuValidator(),
|
|
95
|
+
'sudoku-x': new SudokuXValidator()
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Standard puzzle configurations
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
export const STANDARD_CONFIGS = {
|
|
102
|
+
puzzle4x4: { cageWidthInCells: 2, cageHeightInCells: 2, boardWidthInCages: 2, boardHeightInCages: 2 },
|
|
103
|
+
puzzle6x6: { cageWidthInCells: 3, cageHeightInCells: 2, boardWidthInCages: 2, boardHeightInCages: 3 },
|
|
104
|
+
puzzle9x9: { cageWidthInCells: 3, cageHeightInCells: 3, boardWidthInCages: 3, boardHeightInCages: 3 },
|
|
105
|
+
puzzle12x12: { cageWidthInCells: 4, cageHeightInCells: 3, boardWidthInCages: 3, boardHeightInCages: 4 }
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Factory for creating and validating puzzle definitions
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export class PuzzleDefinitionFactory {
|
|
112
|
+
/**
|
|
113
|
+
* Create a puzzle definition from dimensions and options
|
|
114
|
+
*/
|
|
115
|
+
static create(dimensions, options = {}) {
|
|
116
|
+
var _a, _b, _c, _d, _e;
|
|
117
|
+
const dimensionValidation = this.validate(dimensions);
|
|
118
|
+
if (dimensionValidation.isFailure()) {
|
|
119
|
+
return fail(dimensionValidation.message);
|
|
120
|
+
}
|
|
121
|
+
const totalRows = dimensions.cageHeightInCells * dimensions.boardHeightInCages;
|
|
122
|
+
const totalColumns = dimensions.cageWidthInCells * dimensions.boardWidthInCages;
|
|
123
|
+
const maxValue = dimensions.cageWidthInCells * dimensions.cageHeightInCells;
|
|
124
|
+
const totalCages = dimensions.boardWidthInCages * dimensions.boardHeightInCages;
|
|
125
|
+
const basicCageTotal = this._calculateBasicCageTotal(maxValue);
|
|
126
|
+
// Validate cell data using type-specific validator
|
|
127
|
+
if (options.cells) {
|
|
128
|
+
const puzzleType = (_a = options.type) !== null && _a !== void 0 ? _a : 'sudoku';
|
|
129
|
+
const validator = PUZZLE_TYPE_VALIDATORS[puzzleType];
|
|
130
|
+
if (!validator) {
|
|
131
|
+
return fail(`Unknown puzzle type: ${puzzleType}`);
|
|
132
|
+
}
|
|
133
|
+
const cellValidation = validator.validateCells(options.cells, dimensions);
|
|
134
|
+
if (cellValidation.isFailure()) {
|
|
135
|
+
return fail(cellValidation.message);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const puzzleDefinition = {
|
|
139
|
+
// Dimensions
|
|
140
|
+
cageWidthInCells: dimensions.cageWidthInCells,
|
|
141
|
+
cageHeightInCells: dimensions.cageHeightInCells,
|
|
142
|
+
boardWidthInCages: dimensions.boardWidthInCages,
|
|
143
|
+
boardHeightInCages: dimensions.boardHeightInCages,
|
|
144
|
+
// Derived properties
|
|
145
|
+
totalRows,
|
|
146
|
+
totalColumns,
|
|
147
|
+
maxValue,
|
|
148
|
+
totalCages,
|
|
149
|
+
basicCageTotal,
|
|
150
|
+
// Default values
|
|
151
|
+
id: options.id,
|
|
152
|
+
description: (_b = options.description) !== null && _b !== void 0 ? _b : `${totalRows}x${totalColumns} puzzle`,
|
|
153
|
+
type: (_c = options.type) !== null && _c !== void 0 ? _c : 'sudoku',
|
|
154
|
+
level: (_d = options.level) !== null && _d !== void 0 ? _d : 1,
|
|
155
|
+
cells: (_e = options.cells) !== null && _e !== void 0 ? _e : '.'.repeat(totalRows * totalColumns),
|
|
156
|
+
cages: options.cages
|
|
157
|
+
};
|
|
158
|
+
return succeed(puzzleDefinition);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create killer sudoku puzzle definition with cage constraints
|
|
162
|
+
*/
|
|
163
|
+
static createKiller(dimensions, description) {
|
|
164
|
+
return captureResult(() => {
|
|
165
|
+
// Import needed here to avoid circular dependencies
|
|
166
|
+
const { Ids } = require('./ids');
|
|
167
|
+
// Convert killer cage format to standard cage format - creating minimal cage objects
|
|
168
|
+
const cages = description.killerCages.map((killerCage) => {
|
|
169
|
+
const cellIds = killerCage.cellPositions.map((pos) => Ids.cellId(pos).orThrow());
|
|
170
|
+
return {
|
|
171
|
+
id: `K${killerCage.id}`,
|
|
172
|
+
cellIds,
|
|
173
|
+
cageType: 'killer',
|
|
174
|
+
total: killerCage.sum,
|
|
175
|
+
numCells: cellIds.length,
|
|
176
|
+
/* c8 ignore next 1 - coverage fails when run as part of full suite */
|
|
177
|
+
containsCell: (cellId) => cellIds.includes(cellId)
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
// Return the result directly, not wrapped in another Result
|
|
181
|
+
const result = this.create(dimensions, Object.assign(Object.assign({}, description), { cages, type: 'killer-sudoku' }));
|
|
182
|
+
if (result.isFailure()) {
|
|
183
|
+
throw new Error(result.message);
|
|
184
|
+
}
|
|
185
|
+
return result.value;
|
|
186
|
+
}).onFailure((error) => fail(`Failed to create killer sudoku: ${error}`));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Validate puzzle dimensions
|
|
190
|
+
*/
|
|
191
|
+
static validate(dimensions) {
|
|
192
|
+
// Structural validation
|
|
193
|
+
if (dimensions.cageWidthInCells < 1 || dimensions.cageHeightInCells < 1) {
|
|
194
|
+
return fail('Cage dimensions must be positive integers');
|
|
195
|
+
}
|
|
196
|
+
// Sudoku cages must be multi-cell (both dimensions > 1)
|
|
197
|
+
if (dimensions.cageWidthInCells < 2 || dimensions.cageHeightInCells < 2) {
|
|
198
|
+
return fail('Cage dimensions must be at least 2x2 for valid Sudoku puzzles');
|
|
199
|
+
}
|
|
200
|
+
if (dimensions.boardWidthInCages < 1 || dimensions.boardHeightInCages < 1) {
|
|
201
|
+
return fail('Board dimensions must be positive integers');
|
|
202
|
+
}
|
|
203
|
+
// Size constraints (reasonable limits)
|
|
204
|
+
const totalRows = dimensions.cageHeightInCells * dimensions.boardHeightInCages;
|
|
205
|
+
const totalColumns = dimensions.cageWidthInCells * dimensions.boardWidthInCages;
|
|
206
|
+
if (totalRows > 25 || totalColumns > 25) {
|
|
207
|
+
return fail('Total grid size must not exceed 25x25');
|
|
208
|
+
}
|
|
209
|
+
// Mathematical validity - ensure cage size makes sense for Sudoku constraints
|
|
210
|
+
const maxValue = dimensions.cageWidthInCells * dimensions.cageHeightInCells;
|
|
211
|
+
if (maxValue < 2 || maxValue > 25) {
|
|
212
|
+
return fail('Cage size must result in values between 2 and 25');
|
|
213
|
+
}
|
|
214
|
+
return succeed(true);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get a standard configuration by name
|
|
218
|
+
*/
|
|
219
|
+
static getStandardConfig(name) {
|
|
220
|
+
return STANDARD_CONFIGS[name];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get all available standard configurations
|
|
224
|
+
*/
|
|
225
|
+
static getStandardConfigs() {
|
|
226
|
+
return STANDARD_CONFIGS;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get validator for a specific puzzle type
|
|
230
|
+
*/
|
|
231
|
+
static getValidator(puzzleType) {
|
|
232
|
+
return PUZZLE_TYPE_VALIDATORS[puzzleType];
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Register a custom validator for a puzzle type
|
|
236
|
+
*/
|
|
237
|
+
static registerValidator(puzzleType, validator) {
|
|
238
|
+
PUZZLE_TYPE_VALIDATORS[puzzleType] = validator;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Calculate the basic cage total (sum of 1 to maxValue)
|
|
242
|
+
*/
|
|
243
|
+
static _calculateBasicCageTotal(maxValue) {
|
|
244
|
+
return (maxValue * (maxValue + 1)) / 2;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=puzzleDefinitions.js.map
|