UNPKG

jassl

Version:

Just Another Sudoku Solving Library

203 lines (164 loc) 8.18 kB
const {isUINT, getType} = require('./misc/util'); const SudokuCell = require('./cell'); const SudokuGroup = require('./group'); const SudokuSector = require('./sector'); module.exports = class SudokuPuzzle { constructor(puzzle, sectorSize) { // Puzzle not specified if (!Array.isArray(puzzle)) { throw new Error('INVALID_PUZZLE: puzzle must be an 1-dimensional array'); } // Puzzle must be a square let groupLength = Math.sqrt(puzzle.length); if (!isUINT(groupLength)) { throw new Error('INVALID_PUZZLE: puzzle must be a square: ' + isUINT(groupLength)); } // Every cell value in the puzzle must be an unsigned integer no larger than the length of a row|column|sector if (!puzzle.every(value => value == null || isUINT(value, {max: groupLength}))) { throw new Error('PUZZLE_INVALID: puzzle must only contain unsigned integer values'); } // Sector size not specified if (!sectorSize) { sectorSize = Math.sqrt(groupLength); } // sector size is numeric if (isUINT(sectorSize)) { sectorSize = {width: sectorSize, height: sectorSize}; // sector size invalid } else if (getType(sectorSize) !== 'object') { throw new Error('PUZZLE_INVALID: invalid sector size'); } // sector size does not contain either width or height property if (!sectorSize.width && !sectorSize.height) { sectorSize = Math.sqrt(groupLength); sectorSize = {width: sectorSize, height: sectorSize}; } // only one dimension of the sector specified if (!sectorSize.width) { sectorSize.width = sectorSize.height; } else if (!sectorSize.height) { sectorSize.height = sectorSize.width; } // sector width|height not an unsigned integer if (!isUINT(sectorSize.width)) { throw new Error('PUZZLE_INVALID: sector width not an unsigned integer'); } if (!isUINT(sectorSize.height)) { throw new Error('PUZZLE_INVALID: sector height not an unsigned integer'); } // sector must have same number of cells as does rows and columns if (sectorSize.width * sectorSize.height !== groupLength) { throw new Error('PUZZLE_INVALID: sectors must have the same number of cells as rows and columns'); } // store group length, sector width and sector height this.groupLength = groupLength; this.sectorWidth = sectorSize.width; this.sectorHeight = sectorSize.height; // create a list of all cells in the puzzle this.cells = Array(puzzle.length).fill().map((ignore, index) => new SudokuCell(this, index)); // create a list of rows in the puzzle with references to their respective cells this.rows = Array(groupLength).fill().map((ignore, index) => { return new SudokuGroup('row', this, index, this.cells.slice(index * groupLength, (index + 1) * groupLength)); }); // create a list of columns in the puzzle with references to their respective cells this.columns = Array(groupLength).fill().map((ignore, columnIndex) => { return new SudokuGroup('column', this, columnIndex, Array(groupLength).fill().map((ignore, cellIndex) => this.cells[groupLength * cellIndex + columnIndex])); }); // create list of sectors in the puzzle this.sectors = Array(groupLength).fill().map((ignore, index) => new SudokuSector(this, index)); // load the specified puzzle's values puzzle.forEach((value, index) => { if (isUINT(value, {min: 1})) { this.cells[index].solveTo(value); } }); } get solved() { return this.cells.reduce((solved, cell) => solved && cell.solved, true); } solve() { // puzzle has already been solved if (this.solved) { return true; } // Loop over the puzzle solving cells and removing candidates until the puzzle no longer updates // eslint-disable-next-line no-constant-condition while (!this.solved) { // attempt to solve cells, if any are solved restart the loop if (this.cells.reduce((changed, cell) => cell.solve() || changed, false)) { continue; } // Exclude naked subsets from rows, columns, and sectors // If the puzzle changed, start at top of loop body if (this.sectors.reduce((changed, sector) => sector.excludeNakedSubsets() || changed, this.columns.reduce((changed, column) => column.excludeNakedSubsets() || changed, this.rows.reduce((changed, row) => row.excludeNakedSubsets() || changed, false)))) { continue; } // Exclude candidates based on sector to plane interactions if (this.sectors.reduce((updated, sector) => sector.excludeSectorToPlaneCandidates() || updated, false)) { continue; } // Exclude candidates based on sector to sector interactions if (this.sectors.reduce((updated, sector) => sector.excludeSectorToSectorCandidates() || updated, false)) { continue; } break; } // All avenues to solve have been exhausted, return solve state of puzzle return this.solved; } toArray(unsolvedCells = 0) { return this.cells.map(cell => { return cell.solved ? cell.value : unsolvedCells; }); } toJSON() { return { solved: this.solved, rowCandidates: this.rows.map(row => row.candidates.slice(0)), columnCandidates: this.columns.map(column => column.candidates.slice(0)), sectorCandidates: this.sectors.map(sector => sector.candidates.slice(0)), cells: this.cells.map(cell => { return { solved: cell.solved, rowIndex: cell.row.index, columnIndex: cell.column.index, sectorIndex: cell.sector.index, value: cell.solved ? cell.value : 0, candidates: cell.solved ? [] : cell.candidates.slice(0) }; }) }; } static fromJSON(json) { // generate a new sudoku puzzle from the input json let sudoku = new SudokuPuzzle(json.cells.map(cell => cell.value)); // get a generic list of candidates that cells/rows/columns/sectors may have let candidates = Array(sudoku.groupLength).fill().map((ignore, index) => index + 1); // exclude candidates that do not appear in the input json's candidate list for a cell sudoku.cells.forEach((cell, index) => { candidates .filter(candidate => json.cells[index].candidates.indexOf(candidate) === -1) .forEach(candidate => cell.exclude(candidate)); }); // exclude candidates that do not appear in the input json's candidate list for a row sudoku.rows.forEach((row, index) => { candidates .filter(candidate => json.rowCandidates[index].indexOf(candidate) === -1) .forEach(candidate => row.exclude(candidate)); }); // exclude candidates that do not appear in the input json's candidate list for a column sudoku.columns.forEach((column, index) => { candidates .filter(candidate => json.columnCandidates[index].indexOf(candidate) === -1) .forEach(candidate => column.exclude(candidate)); }); // exclude candidates that do not appear in the input json's candidate list for a sector sudoku.sectors.forEach((sector, index) => { candidates .filter(candidate => json.sectorCandidates[index].indexOf(candidate) === -1) .forEach(candidate => sector.exclude(candidate)); }); // Return puzzle return sudoku; } };