@fgv/ts-sudoku-lib 5.0.0-2 → 5.0.0-21

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 (80) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/config/api-extractor.json +343 -0
  3. package/config/rig.json +16 -0
  4. package/dist/tsdoc-metadata.json +1 -1
  5. package/lib/test/unit/collections.test.d.ts +2 -0
  6. package/lib/test/unit/common/cell.test.d.ts +2 -0
  7. package/lib/test/unit/common/ids.test.d.ts +2 -0
  8. package/lib/test/unit/common/puzzle.test.d.ts +2 -0
  9. package/lib/test/unit/common/puzzleSession.test.d.ts +2 -0
  10. package/lib/test/unit/common/puzzleState.test.d.ts +2 -0
  11. package/lib/test/unit/puzzles/killerSudokuState.test.d.ts +2 -0
  12. package/package.json +14 -14
  13. package/src/index.ts +30 -0
  14. package/src/packlets/collections/collections.ts +124 -0
  15. package/src/packlets/collections/data/puzzles.json +61 -0
  16. package/src/packlets/collections/index.ts +25 -0
  17. package/src/packlets/common/cage.ts +98 -0
  18. package/src/packlets/common/cell.ts +116 -0
  19. package/src/packlets/common/common.ts +120 -0
  20. package/src/packlets/common/converters.ts +84 -0
  21. package/src/packlets/common/ids.ts +98 -0
  22. package/src/packlets/common/index.ts +37 -0
  23. package/src/packlets/common/model.ts +39 -0
  24. package/src/packlets/common/public.ts +100 -0
  25. package/src/packlets/common/puzzle.ts +523 -0
  26. package/src/packlets/common/puzzleSession.ts +415 -0
  27. package/src/packlets/common/puzzleState.ts +108 -0
  28. package/src/packlets/file/converters.ts +49 -0
  29. package/src/packlets/file/index.ts +28 -0
  30. package/src/packlets/file/model.ts +33 -0
  31. package/src/packlets/puzzles/anyPuzzle.ts +50 -0
  32. package/src/packlets/puzzles/index.ts +30 -0
  33. package/src/packlets/puzzles/killerSudokuPuzzle.ts +151 -0
  34. package/src/packlets/puzzles/sudokuPuzzle.ts +46 -0
  35. package/src/packlets/puzzles/sudokuXPuzzle.ts +67 -0
  36. package/CHANGELOG.md +0 -106
  37. package/lib/index.d.ts.map +0 -1
  38. package/lib/index.js.map +0 -1
  39. package/lib/packlets/collections/collections.d.ts.map +0 -1
  40. package/lib/packlets/collections/collections.js.map +0 -1
  41. package/lib/packlets/collections/index.d.ts.map +0 -1
  42. package/lib/packlets/collections/index.js.map +0 -1
  43. package/lib/packlets/common/cage.d.ts.map +0 -1
  44. package/lib/packlets/common/cage.js.map +0 -1
  45. package/lib/packlets/common/cell.d.ts.map +0 -1
  46. package/lib/packlets/common/cell.js.map +0 -1
  47. package/lib/packlets/common/common.d.ts.map +0 -1
  48. package/lib/packlets/common/common.js.map +0 -1
  49. package/lib/packlets/common/converters.d.ts.map +0 -1
  50. package/lib/packlets/common/converters.js.map +0 -1
  51. package/lib/packlets/common/ids.d.ts.map +0 -1
  52. package/lib/packlets/common/ids.js.map +0 -1
  53. package/lib/packlets/common/index.d.ts.map +0 -1
  54. package/lib/packlets/common/index.js.map +0 -1
  55. package/lib/packlets/common/model.d.ts.map +0 -1
  56. package/lib/packlets/common/model.js.map +0 -1
  57. package/lib/packlets/common/public.d.ts.map +0 -1
  58. package/lib/packlets/common/public.js.map +0 -1
  59. package/lib/packlets/common/puzzle.d.ts.map +0 -1
  60. package/lib/packlets/common/puzzle.js.map +0 -1
  61. package/lib/packlets/common/puzzleSession.d.ts.map +0 -1
  62. package/lib/packlets/common/puzzleSession.js.map +0 -1
  63. package/lib/packlets/common/puzzleState.d.ts.map +0 -1
  64. package/lib/packlets/common/puzzleState.js.map +0 -1
  65. package/lib/packlets/file/converters.d.ts.map +0 -1
  66. package/lib/packlets/file/converters.js.map +0 -1
  67. package/lib/packlets/file/index.d.ts.map +0 -1
  68. package/lib/packlets/file/index.js.map +0 -1
  69. package/lib/packlets/file/model.d.ts.map +0 -1
  70. package/lib/packlets/file/model.js.map +0 -1
  71. package/lib/packlets/puzzles/anyPuzzle.d.ts.map +0 -1
  72. package/lib/packlets/puzzles/anyPuzzle.js.map +0 -1
  73. package/lib/packlets/puzzles/index.d.ts.map +0 -1
  74. package/lib/packlets/puzzles/index.js.map +0 -1
  75. package/lib/packlets/puzzles/killerSudokuPuzzle.d.ts.map +0 -1
  76. package/lib/packlets/puzzles/killerSudokuPuzzle.js.map +0 -1
  77. package/lib/packlets/puzzles/sudokuPuzzle.d.ts.map +0 -1
  78. package/lib/packlets/puzzles/sudokuPuzzle.js.map +0 -1
  79. package/lib/packlets/puzzles/sudokuXPuzzle.d.ts.map +0 -1
  80. package/lib/packlets/puzzles/sudokuXPuzzle.js.map +0 -1
@@ -0,0 +1,100 @@
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
+
25
+ import { CageId, CageType, CellId } from './common';
26
+
27
+ /**
28
+ * Describes the structure of a single cage in a {@link PuzzleSession | puzzle}.
29
+ * Does not describe state.
30
+ * @public
31
+ */
32
+ export interface ICage {
33
+ /**
34
+ * Unique identifier for the cage.
35
+ */
36
+ readonly id: CageId;
37
+
38
+ /**
39
+ * The {@link CageType | type} of the cage.
40
+ */
41
+ readonly cageType: CageType;
42
+
43
+ /**
44
+ * The expected sum of all cells in the cage.
45
+ */
46
+ readonly total: number;
47
+
48
+ /**
49
+ * The number of cells in the cage.
50
+ */
51
+ readonly numCells: number;
52
+
53
+ /**
54
+ * The identity of each cell in the cage.
55
+ */
56
+ readonly cellIds: CellId[];
57
+
58
+ /**
59
+ * Determines if a supplied cell is present in the cage.
60
+ * @param id - the identifier to be searched.
61
+ */
62
+ containsCell(id: CellId): boolean;
63
+ }
64
+
65
+ /**
66
+ * Describes the structure of a single cell in a {@link PuzzleSession | puzzle}.
67
+ * Does not describe state.
68
+ * @public
69
+ */
70
+ export interface ICell {
71
+ /**
72
+ * Unique identifier for the cell.
73
+ */
74
+ readonly id: CellId;
75
+
76
+ /**
77
+ * Row number of the cell.
78
+ */
79
+ readonly row: number;
80
+
81
+ /**
82
+ * Column number of the cell.
83
+ */
84
+ readonly col: number;
85
+
86
+ /**
87
+ * All of the {@Link ICage | cages} to which this cell belongs.
88
+ */
89
+ readonly cages: readonly ICage[];
90
+
91
+ /**
92
+ * Indicates whether this cell is a given value (immutable).
93
+ */
94
+ readonly immutable: boolean;
95
+
96
+ /**
97
+ * Given value of this cell, or `undefined` if the cell is not immutable.
98
+ */
99
+ readonly immutableValue?: number;
100
+ }
@@ -0,0 +1,523 @@
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
+
25
+ import { Result, fail, mapResults, succeed } from '@fgv/ts-utils';
26
+ import {
27
+ CageId,
28
+ CellId,
29
+ ICellContents,
30
+ ICellState,
31
+ IRowColumn,
32
+ NavigationDirection,
33
+ NavigationWrap
34
+ } from './common';
35
+
36
+ import { Cage } from './cage';
37
+ import { Cell } from './cell';
38
+ import { Ids } from './ids';
39
+ import { IPuzzleDescription } from './model';
40
+ import { ICell } from './public';
41
+ import { PuzzleState } from './puzzleState';
42
+
43
+ const basicCageTotal: number = 45;
44
+
45
+ /**
46
+ * @internal
47
+ */
48
+ export interface ICellUpdate {
49
+ from: ICellState;
50
+ to: ICellState;
51
+ }
52
+
53
+ /**
54
+ * @internal
55
+ */
56
+ export interface IPuzzleUpdate {
57
+ from: PuzzleState;
58
+ to: PuzzleState;
59
+ cells: ICellUpdate[];
60
+ }
61
+
62
+ /**
63
+ * @internal
64
+ */
65
+ export class Puzzle {
66
+ public readonly id?: string;
67
+ public readonly description: string;
68
+ public readonly initialState: PuzzleState;
69
+
70
+ /**
71
+ * @internal
72
+ */
73
+ protected readonly _rows: Map<CageId, Cage>;
74
+
75
+ /**
76
+ * @internal
77
+ */
78
+ protected readonly _columns: Map<CageId, Cage>;
79
+
80
+ /**
81
+ * @internal
82
+ */
83
+ protected readonly _sections: Map<CageId, Cage>;
84
+
85
+ /**
86
+ * @internal
87
+ */
88
+ protected readonly _cages: Map<CageId, Cage>;
89
+
90
+ /**
91
+ * @internal
92
+ */
93
+ protected readonly _cells: Map<CellId, Cell>;
94
+
95
+ /**
96
+ * Constructs a new puzzle state.
97
+ * @param puzzle - {@Link IPuzzleDescription | Puzzle description} from which this puzzle state
98
+ * is to be initialized.
99
+ */
100
+ protected constructor(puzzle: IPuzzleDescription, extraCages?: [CageId, Cage][]) {
101
+ /* c8 ignore next - ?? is defense in depth */
102
+ extraCages = extraCages ?? [];
103
+
104
+ if (puzzle.rows !== 9) {
105
+ throw new Error(`Puzzle '${puzzle.description}' unsupported row count ${puzzle.rows}`);
106
+ }
107
+ if (puzzle.cols !== 9) {
108
+ throw new Error(`Puzzle '${puzzle.description}' unsupported column count ${puzzle.cols}`);
109
+ }
110
+ if (puzzle.cells.length !== puzzle.rows * puzzle.cols) {
111
+ throw new Error(
112
+ `Puzzle '${puzzle.description}" expected ${puzzle.rows * puzzle.cols} cells, found ${
113
+ puzzle.cells.length
114
+ }`
115
+ );
116
+ }
117
+
118
+ this.id = puzzle.id;
119
+ this.description = puzzle.description;
120
+
121
+ const rows = Puzzle._createRowCages(puzzle.rows, puzzle.cols).orThrow();
122
+ const columns = Puzzle._createColumnCages(puzzle.rows, puzzle.cols).orThrow();
123
+ const sections = Puzzle._createSectionCages(puzzle.rows, puzzle.cols).orThrow();
124
+ const cages = [...rows, ...columns, ...sections, ...extraCages];
125
+ this._rows = new Map(rows);
126
+ this._columns = new Map(columns);
127
+ this._sections = new Map(sections);
128
+ this._cages = new Map(cages);
129
+ this._cells = new Map();
130
+
131
+ const cellInit = [...puzzle.cells];
132
+ for (let row = 0; row < this._rows.size; row++) {
133
+ const rowCage = this.getRow(row).orThrow();
134
+ for (let col = 0; col < this._columns.size; col++) {
135
+ const id = Ids.cellId({ row, col }).orThrow();
136
+ const colCage = this.getColumn(col).orThrow();
137
+ const sectionCage = this.getSection({ row, col }).orThrow();
138
+ const otherCages = extraCages.filter(([__key, cage]) => cage.containsCell(id));
139
+
140
+ const init = cellInit.shift();
141
+ /* c8 ignore next - defense in depth/make type check happy. undefined init should never happen */
142
+ const immutableValue = init === '.' ? undefined : Number.parseInt(init ?? '0');
143
+ if (
144
+ immutableValue !== undefined &&
145
+ (Number.isNaN(immutableValue) || immutableValue < 1 || immutableValue > 9)
146
+ ) {
147
+ throw new Error(`Puzzle ${puzzle.description} illegal value "${init}" for cell ${id}`);
148
+ }
149
+ const cages = [rowCage, colCage, sectionCage, ...otherCages.map(([__key, state]) => state)];
150
+ const cell = new Cell({ id, immutableValue, row, col }, cages);
151
+ this._cells.set(id, cell);
152
+ }
153
+ }
154
+
155
+ this.initialState = PuzzleState.create(
156
+ Array.from(this._cells.entries()).map(([id, state]): ICellState => {
157
+ return { id, value: state.immutableValue, notes: [] };
158
+ })
159
+ ).orThrow();
160
+ }
161
+
162
+ public get numRows(): number {
163
+ return this._rows.size;
164
+ }
165
+
166
+ public get numColumns(): number {
167
+ return this._columns.size;
168
+ }
169
+
170
+ public get rows(): Cage[] {
171
+ return Array.from(this._rows.values());
172
+ }
173
+
174
+ public get cols(): Cage[] {
175
+ return Array.from(this._columns.values());
176
+ }
177
+
178
+ public get sections(): Cage[] {
179
+ return Array.from(this._sections.values());
180
+ }
181
+
182
+ public get cages(): Cage[] {
183
+ return Array.from(this._cages.values());
184
+ }
185
+
186
+ public get cells(): Cell[] {
187
+ return Array.from(this._cells.values());
188
+ }
189
+
190
+ /**
191
+ * @internal
192
+ */
193
+ protected static _createRowCages(numRows: number, numCols: number): Result<[CageId, Cage][]> {
194
+ const cages: [CageId, Cage][] = [];
195
+ for (let r = 0; r < numRows; r++) {
196
+ const id = Ids.rowCageId(r);
197
+ const cellIds = Ids.cellIds(r, 1, 0, numCols);
198
+ /* c8 ignore next 3 - defense in depth should never happen */
199
+ if (cellIds.isFailure()) {
200
+ return fail(cellIds.message);
201
+ }
202
+
203
+ const result = Cage.create(id, 'row', basicCageTotal, cellIds.value).onSuccess((cage) => {
204
+ cages.push([id, cage]);
205
+ return succeed(cage);
206
+ });
207
+ /* c8 ignore next 3 - defense in depth should never happen */
208
+ if (result.isFailure()) {
209
+ return fail(result.message);
210
+ }
211
+ }
212
+ return succeed(cages);
213
+ }
214
+
215
+ /**
216
+ * @internal
217
+ */
218
+ protected static _createColumnCages(numRows: number, numCols: number): Result<[CageId, Cage][]> {
219
+ const cages: [CageId, Cage][] = [];
220
+ for (let c = 0; c < numCols; c++) {
221
+ const id = Ids.columnCageId(c);
222
+ const cellIds = Ids.cellIds(0, numRows, c, 1);
223
+ /* c8 ignore next 3 - defense in depth should never happen */
224
+ if (cellIds.isFailure()) {
225
+ return fail(cellIds.message);
226
+ }
227
+
228
+ const result = Cage.create(id, 'column', basicCageTotal, cellIds.value).onSuccess((cage) => {
229
+ cages.push([id, cage]);
230
+ return succeed(cage);
231
+ });
232
+ /* c8 ignore next 3 - defense in depth should never happen */
233
+ if (result.isFailure()) {
234
+ return fail(result.message);
235
+ }
236
+ }
237
+ return succeed(cages);
238
+ }
239
+
240
+ /**
241
+ * @internal
242
+ */
243
+ private static _createSectionCages(numRows: number, numCols: number): Result<[CageId, Cage][]> {
244
+ const cages: [CageId, Cage][] = [];
245
+ for (let r = 0; r < numRows; r += 3) {
246
+ for (let c = 0; c < numCols; c += 3) {
247
+ const id = Ids.sectionCageId(r, c);
248
+ const cellIds = Ids.cellIds(r, 3, c, 3);
249
+ /* c8 ignore next 3 - defense in depth should never happen */
250
+ if (cellIds.isFailure()) {
251
+ return fail(cellIds.message);
252
+ }
253
+
254
+ const result = Cage.create(id, 'section', basicCageTotal, cellIds.value).onSuccess((cage) => {
255
+ cages.push([id, cage]);
256
+ return succeed(cage);
257
+ });
258
+ /* c8 ignore next 3 - defense in depth should never happen */
259
+ if (result.isFailure()) {
260
+ return fail(result.message);
261
+ }
262
+ }
263
+ }
264
+ return succeed(cages);
265
+ }
266
+
267
+ public checkIsSolved(state: PuzzleState): boolean {
268
+ for (const id of this._cells.keys()) {
269
+ if (!state.hasValue(id)) {
270
+ return false;
271
+ }
272
+ }
273
+ for (const cell of this._cells.values()) {
274
+ if (!cell.isValid(state)) {
275
+ return false;
276
+ }
277
+ }
278
+ return true;
279
+ }
280
+
281
+ public checkIsValid(state: PuzzleState): boolean {
282
+ for (const cell of this._cells.values()) {
283
+ if (!cell.isValid(state)) {
284
+ return false;
285
+ }
286
+ }
287
+ return true;
288
+ }
289
+
290
+ public getEmptyCells(state: PuzzleState): Cell[] {
291
+ return Array.from(this._cells.values()).filter((c) => !state.hasValue(c.id));
292
+ }
293
+
294
+ public getInvalidCells(state: PuzzleState): Cell[] {
295
+ return Array.from(this._cells.values()).filter((c) => !c.isValid(state));
296
+ }
297
+
298
+ public getCellContents(
299
+ spec: string | IRowColumn,
300
+ state: PuzzleState
301
+ ): Result<{ cell: Cell; contents: ICellContents }> {
302
+ return this.getCell(spec).onSuccess((cell) => {
303
+ if (cell.immutable) {
304
+ const contents: ICellContents = { value: cell.immutableValue, notes: [] };
305
+ return succeed({ cell, contents });
306
+ }
307
+ return state.getCellContents(cell.id).onSuccess((contents: ICellContents) => {
308
+ return succeed({ cell, contents });
309
+ });
310
+ });
311
+ }
312
+
313
+ public getCell(spec: string | IRowColumn | ICell): Result<Cell> {
314
+ const want = Ids.cellId(spec);
315
+ if (want.isFailure()) {
316
+ return fail(want.message);
317
+ }
318
+
319
+ const cell = this._cells.get(want.value);
320
+ return cell ? succeed(cell) : fail(`Cell ${want.value} not found`);
321
+ }
322
+
323
+ public getCellNeighbor(
324
+ spec: string | IRowColumn | ICell,
325
+ direction: NavigationDirection,
326
+ wrap: NavigationWrap
327
+ ): Result<ICell> {
328
+ return this.getCell(spec).onSuccess((cell) => {
329
+ const move =
330
+ direction === 'left' || direction === 'right'
331
+ ? this._moveColumn(cell, direction, wrap)
332
+ : this._moveRow(cell, direction, wrap);
333
+ return move.onSuccess((next) => this.getCell(next));
334
+ });
335
+ }
336
+
337
+ public updateContents(wantUpdates: ICellState[] | ICellState, state: PuzzleState): Result<IPuzzleUpdate> {
338
+ wantUpdates = Array.isArray(wantUpdates) ? wantUpdates : [wantUpdates];
339
+ return mapResults(
340
+ wantUpdates.map((u) =>
341
+ this.getCellContents(u.id, state).onSuccess((existing) =>
342
+ existing.cell.update(u.value, u.notes).onSuccess((to) => {
343
+ const from: ICellState = { id: u.id, ...existing.contents };
344
+ return succeed({ from, to });
345
+ })
346
+ )
347
+ )
348
+ ).onSuccess((cellUpdates) => {
349
+ return state.update(cellUpdates.map(({ to }) => to)).onSuccess((updatedState: PuzzleState) => {
350
+ return succeed({
351
+ from: state,
352
+ to: updatedState,
353
+ cells: cellUpdates
354
+ });
355
+ });
356
+ });
357
+ }
358
+
359
+ public updateValues(wantUpdates: ICellState[] | ICellState, state: PuzzleState): Result<IPuzzleUpdate> {
360
+ wantUpdates = Array.isArray(wantUpdates) ? wantUpdates : [wantUpdates];
361
+ return mapResults(
362
+ wantUpdates.map((u) =>
363
+ this.getCellContents(u.id, state).onSuccess((existing) =>
364
+ existing.cell.update(u.value, existing.contents.notes).onSuccess((to) => {
365
+ const from: ICellState = { id: u.id, ...existing.contents };
366
+ return succeed({ from, to });
367
+ })
368
+ )
369
+ )
370
+ ).onSuccess((cellUpdates) => {
371
+ return state.update(cellUpdates.map(({ to }) => to)).onSuccess((updatedState: PuzzleState) => {
372
+ return succeed({
373
+ from: state,
374
+ to: updatedState,
375
+ cells: cellUpdates
376
+ });
377
+ });
378
+ });
379
+ }
380
+
381
+ public updateNotes(wantUpdates: ICellState[] | ICellState, state: PuzzleState): Result<IPuzzleUpdate> {
382
+ wantUpdates = Array.isArray(wantUpdates) ? wantUpdates : [wantUpdates];
383
+ return mapResults(
384
+ wantUpdates.map((u) =>
385
+ this.getCellContents(u.id, state).onSuccess((existing) =>
386
+ existing.cell.update(existing.contents.value, u.notes).onSuccess((to) => {
387
+ const from: ICellState = { id: u.id, ...existing.contents };
388
+ return succeed({ from, to });
389
+ })
390
+ )
391
+ )
392
+ ).onSuccess((cellUpdates) => {
393
+ return state.update(cellUpdates.map(({ to }) => to)).onSuccess((updatedState: PuzzleState) => {
394
+ return succeed({
395
+ from: state,
396
+ to: updatedState,
397
+ cells: cellUpdates
398
+ });
399
+ });
400
+ });
401
+ }
402
+
403
+ public updateCellValue(
404
+ want: string | IRowColumn,
405
+ value: number | undefined,
406
+ state: PuzzleState
407
+ ): Result<IPuzzleUpdate> {
408
+ const idResult = Ids.cellId(want);
409
+ const notes: number[] = [];
410
+ return idResult.onSuccess((id) => {
411
+ const update: ICellState = { id, value, notes };
412
+ return this.updateValues([update], state);
413
+ });
414
+ }
415
+
416
+ public updateCellNotes(
417
+ want: string | IRowColumn,
418
+ notes: number[],
419
+ state: PuzzleState
420
+ ): Result<IPuzzleUpdate> {
421
+ const idResult = Ids.cellId(want);
422
+ const value = undefined;
423
+ return idResult.onSuccess((id) => {
424
+ const update: ICellState = { id, value, notes };
425
+ return this.updateNotes([update], state);
426
+ });
427
+ }
428
+
429
+ public getRow(row: CageId | number): Result<Cage> {
430
+ const id = typeof row === 'number' ? Ids.rowCageId(row) : row;
431
+ const cage = this._rows.get(id);
432
+ return cage ? succeed(cage) : fail(`Row ${id} not found`);
433
+ }
434
+
435
+ public getColumn(col: CageId | number): Result<Cage> {
436
+ const id = typeof col === 'number' ? Ids.columnCageId(col) : col;
437
+ const cage = this._columns.get(id);
438
+ return cage ? succeed(cage) : fail(`Column ${id} not found`);
439
+ }
440
+
441
+ public getSection(spec: CageId | IRowColumn): Result<Cage> {
442
+ const id = typeof spec === 'object' ? Ids.sectionCageId(spec.row, spec.col) : spec;
443
+ const cage = this._sections.get(id);
444
+ return cage ? succeed(cage) : fail(`Section ${id} not found`);
445
+ }
446
+
447
+ public getCage(id: CageId): Result<Cage> {
448
+ const cage = this._cages.get(id);
449
+ return cage ? succeed(cage) : fail(`Cage ${id} not found`);
450
+ }
451
+
452
+ public toStrings(state: PuzzleState): string[] {
453
+ return Array.from(this._rows.values()).map((row) => row.toString(state));
454
+ }
455
+
456
+ public toString(state: PuzzleState): string {
457
+ return this.toStrings(state).join('\n');
458
+ }
459
+
460
+ private _moveColumn(
461
+ current: IRowColumn,
462
+ direction: 'left' | 'right',
463
+ wrap: NavigationWrap
464
+ ): Result<IRowColumn> {
465
+ const row = current.row;
466
+
467
+ let col = current.col;
468
+ if (direction === 'left') {
469
+ if (col > 0) {
470
+ col = col - 1;
471
+ } else if (wrap !== 'none') {
472
+ col = this.numColumns - 1;
473
+ if (wrap === 'wrap-next') {
474
+ return this._moveRow({ row, col }, 'up', 'wrap-around');
475
+ }
476
+ } else {
477
+ return fail(`cannot move left from column ${current.col}`);
478
+ }
479
+ } else if (direction === 'right') {
480
+ if (col < this.numColumns - 1) {
481
+ col = col + 1;
482
+ } else if (wrap !== 'none') {
483
+ col = 0;
484
+ if (wrap === 'wrap-next') {
485
+ return this._moveRow({ row, col }, 'down', 'wrap-around');
486
+ }
487
+ } else {
488
+ return fail(`cannot move right from column ${current.col}`);
489
+ }
490
+ }
491
+ return succeed({ row, col });
492
+ }
493
+
494
+ private _moveRow(current: IRowColumn, direction: 'up' | 'down', wrap: NavigationWrap): Result<IRowColumn> {
495
+ let row = current.row;
496
+ const col = current.col;
497
+
498
+ if (direction === 'up') {
499
+ if (row > 0) {
500
+ row = row - 1;
501
+ } else if (wrap !== 'none') {
502
+ row = this.numRows - 1;
503
+ if (wrap === 'wrap-next') {
504
+ return this._moveColumn({ row, col }, 'left', 'wrap-around');
505
+ }
506
+ } else {
507
+ return fail(`cannot move up from row ${current.row}`);
508
+ }
509
+ } else if (direction === 'down') {
510
+ if (row < this.numRows - 1) {
511
+ row = row + 1;
512
+ } else if (wrap !== 'none') {
513
+ row = 0;
514
+ if (wrap === 'wrap-next') {
515
+ return this._moveColumn({ row, col }, 'right', 'wrap-around');
516
+ }
517
+ } else {
518
+ return fail(`cannot move down from row ${current.row}`);
519
+ }
520
+ }
521
+ return succeed({ row, col });
522
+ }
523
+ }