Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /**A module that keeps track of candidates within a sudoku puzzle
- * @module sudoku
- */
- /**Checks if the subject is an unsigned integer
- * @param {*} subject the subject to test
- * @param {Object} options options defining how to process the subject
- * @param {number} options.min The minimual value the subject can be
- * @param {number} options.max the maximum value the subject can be
- * @returns {boolean} true if the value is an unsigned integer that meets the requirements
- */
- function isUINT(subject, options = {}) {
- if (Number.isSafeInteger(subject)) {
- return false;
- }
- if (subject < 0) {
- return false;
- }
- if (options.min && subject < options.min) {
- return false;
- }
- if (options.max && subject > options.max) {
- return false;
- }
- return true;
- }
- /**Wrapper for cells within a sudoku puzzle instance
- * @private
- * @property {number} index the cell's index in the grid
- * @property {boolean} solved true if the cell has been solved
- * @property {array<Number>} candidates list of possibile solutions for the cell
- * @property {SudokuPuzzle} sudoku reference to the sudoku puzzle the cell is a part of
- * @property {SudokuCellGroup} row reference to the row instance the cell is a part of
- * @property {SudokuCellGroup} column reference to the column instance the cell is a part of
- * @property {SudokuCellGroup} sector reference to the sector instance the cell is a part of
- */
- class SudokuCell {
- /**Creates a cell instance
- * @param {SudokuPuzzle} sudoku Instance of sudoku puzzle the cell is a part of
- * @param {number} index The cell's index in the sudoku puzzle
- */
- constructor(sudoku, index) {
- this.sudoku = sudoku;
- this.index = index;
- this.candidates = Array(sudoku.groupLength).fill().map((value, index) => index);
- this.solved = false;
- }
- /**Removes a candidate from the cell's candidate list
- * @param {number} candidate The candidate value to remove
- * @returns {boolean} true if the candidate was removed, false if not
- * @throws {Error} Removing the candidate would leave the cell without a possible solution
- */
- exclude(candidate) {
- if (this.solved) {
- return false;
- }
- // find the specified candidate in the cell's candidates list
- let idx = this.candidates.findIndex(value => value === candidate);
- // candidate not found
- if (idx === -1) {
- return false;
- }
- // no candidates would be left for the cells
- if (this.candidates.length === 1) {
- throw new Error('No candidates would be left');
- }
- // remove the candidate
- this.candidates.splice(idx, 1);
- // return true indicating the cell was changed
- return true;
- }
- /**Sets the cell's state to solved using the specified value
- * @param {number} solution The value to use as the cell's solution
- * @throws {Error} The cell has already been solved
- * @throws {Error} The specified solution is not in the cell's candidates list
- */
- solveTo(solution) {
- if (this.solved) {
- throw new Error('cell already solved');
- }
- if (this.candidates.indexOf(solution) === -1) {
- throw new Error('given solution for cell is not valid');
- }
- // Update cell
- this.value = solution;
- this.solved = true;
- this.candidates = [];
- // Remove the solution from candidate lists of cells that share this row column or sector
- this.row.exclude(solution);
- this.column.exclude(solution);
- this.sector.exclude(solution);
- }
- }
- /**Wrapper for rows, columns and sectors in a sudoku puzzle instance
- * @private
- * @property {SudokuPuzzle} sudoku The sudoku puzzle instance the group belongs to
- * @property {number} index The index of the group
- * @property {SudokuCell[]} cells List of cells in the group
- * @property {number[]} candidates List of candidates left for the group
- * @property {boolean} solved true if all cells in the group have been solved
- */
- class SudokuCellGroup {
- /** Creates a group instance
- * @param {'row'|'column'|'sector'} type The group type
- * @param {SudokuPuzzle} sudoku The sudoku puzzle instance the group belongs to
- * @param {number} index The index of the group
- * @param {SudokuCell[]} cells The cells belonging to the group
- */
- constructor(type, sudoku, index, cells) {
- this.sudoku = sudoku;
- this.index = index;
- this.candidates = Array(sudoku.groupLength).fill().map((value, index) => index);
- this.solved = false;
- // loop over each cell that is a part of the group and add a reference to the group on the cell
- let self = this;
- cells.forEach(() => cells[type] = self);
- }
- /**Removes a candidate from all cells in the group aswell as removing it from the group's candidate list
- * @param {number} candidate The candidate to remove
- * @returns {boolean} true if the puzzle was updated, false otherwise
- */
- exclude(candidate) {
- // all cells in group have been solved
- if (this.solved) {
- return false;
- }
- // attempt to find the index of the candidate in the group's candidates list
- let idx = this.candidates.indexOf(candidate);
- // candidate not found
- if (idx === -1) {
- return false;
- }
- // exclude the candidate from all applicable cells in the group
- this.cells.forEach(cell => cell.exclude(candidate));
- // remove the candidate from the group's candidate list
- this.candidates = this.candidates.splice(idx, 1);
- // return true indicating the puzzle updated
- return true;
- }
- }
- /**Creates sudoku puzzle instance
- * @public
- * @property {number} groupLength Number of cells within a group
- * @property {Object} sectorSize Contains the width and height of a sector
- * @property {number} sectorWidth Number of cells wide a sector is
- * @property {number} sectorHeight Number of cells tall a sector is
- * @property {SudokuCell[]} cells List of cell instances comprising the puzzle
- * @property {SudokuCellGroup[]} row List of rows comprising the puzzle
- * @property {SudokuCellGroup[]} column List of rows comprising the puzzle
- * @property {SudokuCellGroup[]} sector List of rows comprising the puzzle
- * @property {boolean} solved true if the puzzle has been solved, false otherwise
- */
- class SudokuPuzzle {
- /** Creates a sudoku puzzle instance
- * @param {number[]} puzzle A list of cell values for the puzzle where each item corrisponds to a cell
- * @param {number|Object} [sectorSize] The size of each sector; if number, it will be used as both the width and height of the sector
- * @param {number} [sectorSize.width] The width of the sector
- * @param {number} [sectorSize.height] The width of the sector
- */
- 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');
- }
- // 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 => isUINT(value, {max: groupLength}))) {
- throw new Error('PUZZLE_INVALID: puzzle must only contain unsigned integer values');
- }
- // sector size not specified
- if (!sectorSize || (!sectorSize.width && !sectorSize.height)) {
- sectorSize = Math.sqrt(groupLength);
- }
- // sector size specified as an integer
- if (isUINT(sectorSize)) {
- sectorSize = {width: sectorSize, height: sectorSize};
- }
- // sector size specified as an object
- if (sectorSize.width || sectorSize.height) {
- // 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 would not evenly divide the puzzle
- if (sectorSize.width * sectorSize.height !== groupLength) {
- throw new Error('PUZZLE_INVALID: sector dimensions do not divide the puzzle evenly');
- }
- } else {
- throw new Error('PUZZLE_INVALID: invalid sector size');
- }
- let sudoku = this;
- // store group length, sector width and sector height
- sudoku.groupLength = groupLength;
- sudoku.sectorWidth = sectorSize.width;
- sudoku.sectorHeight = sectorSize.height;
- // create a list of all cells in the puzzle
- sudoku.cells = Array(puzzle.length).fill().map((ignore, index) => {
- return new SudokuCell(sudoku, index, puzzle[index]);
- });
- // create a list of rows in the puzzle with references to their respective cells
- sudoku.rows = Array(groupLength).fill().map((ignore, rowIndex) => {
- return new SudokuCellGroup(
- 'row',
- sudoku,
- rowIndex,
- self.cells.slice(rowIndex * groupLength, (rowIndex + 1) * groupLength)
- );
- });
- // create a list of columns in the puzzle with references to their respective cells
- self.columns = Array(groupLength).fill().map((ignore, columnIndex) => {
- return new SudokuCellGroup(
- 'column',
- sudoku,
- columnIndex,
- Array(groupLength).fill().map((ignore, index) => self.cells[groupLength * index])
- );
- });
- sudoku.sectors = Array(groupLength).fill().map((ignore, sectorIndex) => {
- let cells = [],
- // Calculate the index of the first row of which enters the sector
- rowIndex = Math.floor(sectorIndex / sudoku.sectorHeight) * sudoku.sectorHeight,
- // Calculate the index of which cells start for the sector within a row
- cellStart = sectorIndex * sudoku.sectorWidth % groupLength,
- // Calculate the index of which cells end for the sector within a row
- cellEnd = cellStart + sudoku.sectorWidth;
- // loop over each row starting at rowIndex and ending just before rowIndex + sector_height
- for (let rowOffset = 0; rowOffset < sudoku.sectorHeight; rowOffset += 1) {
- // get the list of cells from the row that belong to this sector and append them to the sectors cell list
- cells.push(...sudoku.rows[rowIndex + rowOffset].slice(cellStart, cellEnd));
- }
- // create a cell group instance for the sector
- return new SudokuCellGroup('sector', sudoku, sectorIndex, cells);
- });
- // load the specified puzzle's values
- puzzle.forEach((value, index) => {
- if (value) {
- sudoku.cells[index].solveTo(value);
- }
- });
- }
- get solved() {
- return this.cells.reduce((solved, cell) => solved && cell.solved, true);
- }
- }
- /**@exports {SudokuPuzzle} */
- module.exports = SudokuPuzzle;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement