@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.
Files changed (132) hide show
  1. package/debug_killer.js +50 -0
  2. package/debug_test.js +88 -0
  3. package/dist/index.browser.js +37 -0
  4. package/dist/index.js +30 -0
  5. package/dist/packlets/collections/collections.js +136 -0
  6. package/dist/packlets/collections/data/puzzles.json +56 -0
  7. package/dist/packlets/collections/index.js +25 -0
  8. package/dist/packlets/common/cage.js +83 -0
  9. package/dist/packlets/common/cell.js +88 -0
  10. package/dist/packlets/common/common.js +70 -0
  11. package/dist/packlets/common/converters.js +65 -0
  12. package/dist/packlets/common/ids.js +152 -0
  13. package/dist/packlets/common/index.js +36 -0
  14. package/dist/packlets/common/logging.js +32 -0
  15. package/dist/packlets/common/public.js +25 -0
  16. package/dist/packlets/common/puzzle.js +411 -0
  17. package/dist/packlets/common/puzzleDefinitions.js +247 -0
  18. package/dist/packlets/common/puzzleSession.js +369 -0
  19. package/dist/packlets/common/puzzleState.js +99 -0
  20. package/dist/packlets/files/converters.js +67 -0
  21. package/dist/packlets/files/fileTreeHelpers.js +37 -0
  22. package/dist/packlets/files/filesystem.js +37 -0
  23. package/dist/packlets/files/index.browser.js +32 -0
  24. package/dist/packlets/files/index.js +30 -0
  25. package/dist/packlets/files/model.js +25 -0
  26. package/dist/packlets/hints/baseHintProvider.js +199 -0
  27. package/dist/packlets/hints/explanations.js +270 -0
  28. package/dist/packlets/hints/hiddenSingles.js +233 -0
  29. package/dist/packlets/hints/hintRegistry.js +222 -0
  30. package/dist/packlets/hints/hints.js +311 -0
  31. package/dist/packlets/hints/index.js +39 -0
  32. package/dist/packlets/hints/interfaces.js +25 -0
  33. package/dist/packlets/hints/nakedSingles.js +198 -0
  34. package/dist/packlets/hints/puzzleSessionHints.js +495 -0
  35. package/dist/packlets/hints/types.js +43 -0
  36. package/dist/packlets/puzzles/anyPuzzle.js +47 -0
  37. package/dist/packlets/puzzles/index.js +31 -0
  38. package/dist/packlets/puzzles/internal/combinationCache.js +75 -0
  39. package/dist/packlets/puzzles/internal/combinationGenerator.js +142 -0
  40. package/dist/packlets/puzzles/internal/possibilityAnalyzer.js +121 -0
  41. package/dist/packlets/puzzles/killerCombinations.js +222 -0
  42. package/dist/packlets/puzzles/killerCombinationsTypes.js +25 -0
  43. package/dist/packlets/puzzles/killerSudokuPuzzle.js +136 -0
  44. package/dist/packlets/puzzles/sudokuPuzzle.js +43 -0
  45. package/dist/packlets/puzzles/sudokuXPuzzle.js +65 -0
  46. package/dist/test/unit/helpers/puzzleBuilders.js +149 -0
  47. package/dist/ts-sudoku-lib.d.ts +1515 -47
  48. package/dist/tsdoc-metadata.json +1 -1
  49. package/lib/index.browser.d.ts +7 -0
  50. package/lib/index.browser.js +78 -0
  51. package/lib/index.d.ts +3 -2
  52. package/lib/index.js +5 -3
  53. package/lib/packlets/collections/collections.d.ts +15 -8
  54. package/lib/packlets/collections/collections.js +41 -11
  55. package/lib/packlets/collections/data/puzzles.json +28 -33
  56. package/lib/packlets/common/common.d.ts +15 -0
  57. package/lib/packlets/common/common.js +25 -0
  58. package/lib/packlets/common/converters.d.ts +0 -6
  59. package/lib/packlets/common/converters.js +17 -22
  60. package/lib/packlets/common/ids.d.ts +8 -1
  61. package/lib/packlets/common/ids.js +83 -10
  62. package/lib/packlets/common/index.d.ts +3 -2
  63. package/lib/packlets/common/index.js +5 -2
  64. package/lib/packlets/common/logging.d.ts +7 -0
  65. package/lib/packlets/common/logging.js +35 -0
  66. package/lib/packlets/common/puzzle.d.ts +23 -13
  67. package/lib/packlets/common/puzzle.js +51 -25
  68. package/lib/packlets/common/puzzleDefinitions.d.ts +127 -0
  69. package/lib/packlets/common/puzzleDefinitions.js +251 -0
  70. package/lib/packlets/common/puzzleSession.d.ts +8 -0
  71. package/lib/packlets/common/puzzleSession.js +12 -0
  72. package/lib/packlets/common/puzzleState.js +3 -0
  73. package/lib/packlets/files/converters.d.ts +27 -0
  74. package/lib/packlets/files/converters.js +71 -0
  75. package/lib/packlets/files/fileTreeHelpers.d.ts +13 -0
  76. package/lib/packlets/files/fileTreeHelpers.js +40 -0
  77. package/lib/packlets/{file/converters.d.ts → files/filesystem.d.ts} +3 -8
  78. package/lib/packlets/{file/converters.js → files/filesystem.js} +5 -14
  79. package/lib/packlets/{file/index.d.ts → files/index.browser.d.ts} +2 -1
  80. package/lib/packlets/{file/index.js → files/index.browser.js} +9 -3
  81. package/lib/packlets/files/index.d.ts +6 -0
  82. package/lib/packlets/files/index.js +69 -0
  83. package/lib/packlets/files/model.d.ts +32 -0
  84. package/lib/packlets/hints/baseHintProvider.d.ts +103 -0
  85. package/lib/packlets/hints/baseHintProvider.js +203 -0
  86. package/lib/packlets/hints/explanations.d.ts +105 -0
  87. package/lib/packlets/hints/explanations.js +276 -0
  88. package/lib/packlets/hints/hiddenSingles.d.ts +81 -0
  89. package/lib/packlets/hints/hiddenSingles.js +237 -0
  90. package/lib/packlets/hints/hintRegistry.d.ts +95 -0
  91. package/lib/packlets/hints/hintRegistry.js +226 -0
  92. package/lib/packlets/hints/hints.d.ts +144 -0
  93. package/lib/packlets/hints/hints.js +316 -0
  94. package/lib/packlets/hints/index.d.ts +10 -0
  95. package/lib/packlets/hints/index.js +55 -0
  96. package/lib/packlets/hints/interfaces.d.ts +121 -0
  97. package/lib/packlets/{file/model.js → hints/interfaces.js} +1 -1
  98. package/lib/packlets/hints/nakedSingles.d.ts +57 -0
  99. package/lib/packlets/hints/nakedSingles.js +202 -0
  100. package/lib/packlets/hints/puzzleSessionHints.d.ts +306 -0
  101. package/lib/packlets/hints/puzzleSessionHints.js +499 -0
  102. package/lib/packlets/hints/types.d.ts +102 -0
  103. package/lib/packlets/hints/types.js +46 -0
  104. package/lib/packlets/puzzles/anyPuzzle.d.ts +3 -3
  105. package/lib/packlets/puzzles/anyPuzzle.js +1 -1
  106. package/lib/packlets/puzzles/index.d.ts +4 -2
  107. package/lib/packlets/puzzles/index.js +20 -3
  108. package/lib/packlets/puzzles/internal/combinationCache.d.ts +29 -0
  109. package/lib/packlets/puzzles/internal/combinationCache.js +79 -0
  110. package/lib/packlets/puzzles/internal/combinationGenerator.d.ts +34 -0
  111. package/lib/packlets/puzzles/internal/combinationGenerator.js +146 -0
  112. package/lib/packlets/puzzles/internal/possibilityAnalyzer.d.ts +25 -0
  113. package/lib/packlets/puzzles/internal/possibilityAnalyzer.js +125 -0
  114. package/lib/packlets/puzzles/killerCombinations.d.ts +90 -0
  115. package/lib/packlets/puzzles/killerCombinations.js +226 -0
  116. package/lib/packlets/puzzles/killerCombinationsTypes.d.ts +17 -0
  117. package/lib/packlets/puzzles/killerCombinationsTypes.js +26 -0
  118. package/lib/packlets/puzzles/killerSudokuPuzzle.d.ts +2 -2
  119. package/lib/packlets/puzzles/killerSudokuPuzzle.js +15 -11
  120. package/lib/packlets/puzzles/sudokuPuzzle.d.ts +2 -2
  121. package/lib/packlets/puzzles/sudokuXPuzzle.d.ts +2 -2
  122. package/lib/packlets/puzzles/sudokuXPuzzle.js +8 -4
  123. package/package.json +32 -18
  124. package/sample-12x12-puzzle.js +72 -0
  125. package/sample-4x4-puzzles.js +68 -0
  126. package/sample-6x6-puzzle.js +61 -0
  127. package/temp_12x12_cells.txt +12 -0
  128. package/temp_correct.txt +12 -0
  129. package/test-grid-sizes.js +132 -0
  130. package/lib/packlets/common/model.d.ts +0 -15
  131. package/lib/packlets/file/model.d.ts +0 -9
  132. /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