@fgv/ts-sudoku-lib 5.0.1-9 → 5.0.2-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.browser.js +37 -0
  2. package/dist/index.js +30 -0
  3. package/dist/packlets/collections/collections.js +136 -0
  4. package/dist/packlets/collections/data/puzzles.json +56 -0
  5. package/dist/packlets/collections/index.js +25 -0
  6. package/dist/packlets/common/cage.js +83 -0
  7. package/dist/packlets/common/cell.js +88 -0
  8. package/dist/packlets/common/common.js +70 -0
  9. package/dist/packlets/common/converters.js +65 -0
  10. package/dist/packlets/common/ids.js +152 -0
  11. package/dist/packlets/common/index.js +36 -0
  12. package/dist/packlets/common/logging.js +32 -0
  13. package/dist/packlets/common/public.js +25 -0
  14. package/dist/packlets/common/puzzle.js +411 -0
  15. package/dist/packlets/common/puzzleDefinitions.js +247 -0
  16. package/dist/packlets/common/puzzleSession.js +369 -0
  17. package/dist/packlets/common/puzzleState.js +99 -0
  18. package/dist/packlets/files/converters.js +67 -0
  19. package/dist/packlets/files/fileTreeHelpers.js +37 -0
  20. package/dist/packlets/files/filesystem.js +37 -0
  21. package/dist/packlets/files/index.browser.js +32 -0
  22. package/dist/packlets/files/index.js +30 -0
  23. package/dist/packlets/files/model.js +25 -0
  24. package/dist/packlets/hints/baseHintProvider.js +199 -0
  25. package/dist/packlets/hints/explanations.js +270 -0
  26. package/dist/packlets/hints/hiddenSingles.js +233 -0
  27. package/dist/packlets/hints/hintRegistry.js +222 -0
  28. package/dist/packlets/hints/hints.js +311 -0
  29. package/dist/packlets/hints/index.js +39 -0
  30. package/dist/packlets/hints/interfaces.js +25 -0
  31. package/dist/packlets/hints/nakedSingles.js +198 -0
  32. package/dist/packlets/hints/puzzleSessionHints.js +495 -0
  33. package/dist/packlets/hints/types.js +43 -0
  34. package/dist/packlets/puzzles/anyPuzzle.js +47 -0
  35. package/dist/packlets/puzzles/index.js +31 -0
  36. package/dist/packlets/puzzles/internal/combinationCache.js +75 -0
  37. package/dist/packlets/puzzles/internal/combinationGenerator.js +142 -0
  38. package/dist/packlets/puzzles/internal/possibilityAnalyzer.js +121 -0
  39. package/dist/packlets/puzzles/killerCombinations.js +222 -0
  40. package/dist/packlets/puzzles/killerCombinationsTypes.js +25 -0
  41. package/dist/packlets/puzzles/killerSudokuPuzzle.js +136 -0
  42. package/dist/packlets/puzzles/sudokuPuzzle.js +43 -0
  43. package/dist/packlets/puzzles/sudokuXPuzzle.js +65 -0
  44. package/dist/test/unit/helpers/puzzleBuilders.js +149 -0
  45. package/dist/ts-sudoku-lib.d.ts +12 -7
  46. package/dist/tsdoc-metadata.json +1 -1
  47. package/lib/index.browser.d.ts +7 -0
  48. package/lib/index.browser.js +78 -0
  49. package/lib/packlets/files/fileTreeHelpers.d.ts +13 -0
  50. package/lib/packlets/files/fileTreeHelpers.js +40 -0
  51. package/lib/packlets/files/index.browser.d.ts +5 -0
  52. package/lib/packlets/files/index.browser.js +70 -0
  53. package/lib/packlets/files/index.d.ts +3 -2
  54. package/lib/packlets/files/index.js +7 -4
  55. package/package.json +23 -5
@@ -0,0 +1,152 @@
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 * as Converters from './converters';
26
+ const firstRowIdChar = 'A'.charCodeAt(0);
27
+ /**
28
+ * Convert a column index (0-based) to column letters (A-Z, AA-AZ, BA-BZ, etc.)
29
+ * @param col - 0-based column index
30
+ * @returns Column identifier string
31
+ */
32
+ function columnToLetters(col) {
33
+ if (col < 26) {
34
+ // Single letter: A-Z
35
+ return String.fromCharCode(firstRowIdChar + col);
36
+ /* c8 ignore next 8 - functional code tested but coverage intermittently missed */
37
+ }
38
+ else {
39
+ // Double letters: AA, AB, ..., ZZ
40
+ const firstLetter = Math.floor(col / 26) - 1;
41
+ const secondLetter = col % 26;
42
+ return (String.fromCharCode(firstRowIdChar + firstLetter) + String.fromCharCode(firstRowIdChar + secondLetter));
43
+ }
44
+ }
45
+ /**
46
+ * Convert a row index (0-based) to row number string (1-based with zero-padding)
47
+ * @param row - 0-based row index
48
+ * @param maxRow - Maximum row index (optional, for determining padding width)
49
+ * @returns Row number string with zero-padding
50
+ */
51
+ function rowToNumber(row, maxRow) {
52
+ const rowNum = row + 1;
53
+ /* c8 ignore next 5 - functional code tested but coverage intermittently missed */
54
+ if (maxRow !== undefined) {
55
+ const maxRowNum = maxRow + 1;
56
+ const width = maxRowNum.toString().length;
57
+ return rowNum.toString().padStart(width, '0');
58
+ }
59
+ // Default: pad to 2 digits for grids up to 99x99
60
+ return rowNum.toString().padStart(2, '0');
61
+ }
62
+ /**
63
+ * Parse a cell ID string back to row/column coordinates
64
+ * @param cellId - Cell ID string (e.g., "A1", "A01", "AB15")
65
+ * @returns Row and column indices (0-based) or undefined if invalid
66
+ * @public
67
+ */
68
+ export function parseCellId(cellId) {
69
+ const match = cellId.match(/^([A-Z]{1,2})([0-9]{1,3})$/);
70
+ /* c8 ignore next 3 - functional code tested but coverage intermittently missed */
71
+ if (!match) {
72
+ return undefined;
73
+ }
74
+ const rowLetters = match[1];
75
+ const colNumber = parseInt(match[2], 10);
76
+ let row;
77
+ if (rowLetters.length === 1) {
78
+ // Single letter: A-Z maps to rows 0-25
79
+ row = rowLetters.charCodeAt(0) - firstRowIdChar;
80
+ /* c8 ignore next 6 - functional code tested but coverage intermittently missed */
81
+ }
82
+ else {
83
+ // Double letters: AA-ZZ for rows 26+
84
+ const firstLetter = rowLetters.charCodeAt(0) - firstRowIdChar;
85
+ const secondLetter = rowLetters.charCodeAt(1) - firstRowIdChar;
86
+ row = (firstLetter + 1) * 26 + secondLetter;
87
+ }
88
+ const col = colNumber - 1; // Convert from 1-based to 0-based
89
+ return { row, col };
90
+ }
91
+ function isRowColumn(from) {
92
+ return typeof from === 'object' && from !== null && 'row' in from && `col` in from;
93
+ }
94
+ /**
95
+ * @public
96
+ */
97
+ export class Ids {
98
+ static cageId(from) {
99
+ if (typeof from === 'string') {
100
+ return Converters.cageId.convert(from);
101
+ }
102
+ /* c8 ignore next 2 - functional code tested but coverage intermittently missed */
103
+ return succeed(from.id);
104
+ }
105
+ static cellId(spec) {
106
+ if (isRowColumn(spec)) {
107
+ if ('id' in spec) {
108
+ return succeed(spec.id);
109
+ }
110
+ // Standard sudoku convention: Letters for rows (A-I), numbers for columns (1-9)
111
+ const rowLetter = columnToLetters(spec.row);
112
+ // For 9x9 grids, use single digit format for backward compatibility
113
+ // For larger grids, use zero-padding
114
+ const colNumber = spec.col < 9 ? (spec.col + 1).toString() : rowToNumber(spec.col);
115
+ return succeed(`${rowLetter}${colNumber}`);
116
+ }
117
+ return Converters.cellId.convert(spec);
118
+ }
119
+ static rowCageId(row) {
120
+ return `R${columnToLetters(row)}`;
121
+ }
122
+ static columnCageId(col) {
123
+ // For backward compatibility, use single digit for columns < 9
124
+ const colNumber = col < 9 ? (col + 1).toString() : rowToNumber(col);
125
+ return `C${colNumber}`;
126
+ }
127
+ static sectionCageId(row, col, cageHeight = 3, cageWidth = 3) {
128
+ row = Math.floor(row / cageHeight) * cageHeight;
129
+ col = Math.floor(col / cageWidth) * cageWidth;
130
+ // Section IDs use row letter and column number
131
+ const rowLetter = columnToLetters(row);
132
+ const colNumber = col < 9 ? (col + 1).toString() : rowToNumber(col);
133
+ return `S${rowLetter}${colNumber}`;
134
+ }
135
+ static cellIds(firstRow, numRows, firstCol, numCols) {
136
+ const cellIds = [];
137
+ for (let row = firstRow; row < firstRow + numRows; row++) {
138
+ for (let col = firstCol; col < firstCol + numCols; col++) {
139
+ const result = this.cellId({ row, col }).onSuccess((id) => {
140
+ cellIds.push(id);
141
+ return succeed(id);
142
+ });
143
+ /* c8 ignore next 3 - defense in depth should not happen */
144
+ if (result.isFailure()) {
145
+ return fail(result.message);
146
+ }
147
+ }
148
+ }
149
+ return succeed(cellIds);
150
+ }
151
+ }
152
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1,36 @@
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 * as Converters from './converters';
25
+ export * from './cage';
26
+ export * from './cell';
27
+ export * from './common';
28
+ export { Ids, parseCellId } from './ids';
29
+ export { DefaultSudokuLogger } from './logging';
30
+ export * from './public';
31
+ export * from './puzzle';
32
+ export * from './puzzleDefinitions';
33
+ export * from './puzzleSession';
34
+ export * from './puzzleState';
35
+ export { Converters };
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,32 @@
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 { Logging } from '@fgv/ts-utils';
25
+ /**
26
+ * Default no-op logger for use when diagnostic logging is not needed.
27
+ * @public
28
+ */
29
+ export const DefaultSudokuLogger = new Logging.LogReporter({
30
+ logger: new Logging.NoOpLogger()
31
+ });
32
+ //# sourceMappingURL=logging.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=public.js.map
@@ -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