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

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.
@@ -1,523 +0,0 @@
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
- }