Advertisement
SReject

Sudoku Candidate Tracking

Aug 15th, 2019
217
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**A module that keeps track of candidates within a sudoku puzzle
  2.  * @module sudoku
  3.  */
  4.  
  5. /**Checks if the subject is an unsigned integer
  6.  * @param {*} subject the subject to test
  7.  * @param {Object} options options defining how to process the subject
  8.  * @param {number} options.min The minimual value the subject can be
  9.  * @param {number} options.max the maximum value the subject can be
  10.  * @returns {boolean} true if the value is an unsigned integer that meets the requirements
  11. */
  12. function isUINT(subject, options = {}) {
  13.     if (Number.isSafeInteger(subject)) {
  14.         return false;
  15.     }
  16.     if (subject < 0) {
  17.         return false;
  18.     }
  19.     if (options.min && subject < options.min) {
  20.         return false;
  21.     }
  22.     if (options.max && subject > options.max) {
  23.         return false;
  24.     }
  25.     return true;
  26. }
  27.  
  28.  
  29.  
  30. /**Wrapper for cells within a sudoku puzzle instance
  31.  * @private
  32.  * @property {number} index the cell's index in the grid
  33.  * @property {boolean} solved true if the cell has been solved
  34.  * @property {array<Number>} candidates list of possibile solutions for the cell
  35.  * @property {SudokuPuzzle} sudoku reference to the sudoku puzzle the cell is a part of
  36.  * @property {SudokuCellGroup} row reference to the row instance the cell is a part of
  37.  * @property {SudokuCellGroup} column reference to the column instance the cell is a part of
  38.  * @property {SudokuCellGroup} sector reference to the sector instance the cell is a part of
  39.  */
  40. class SudokuCell {
  41.  
  42.     /**Creates a cell instance
  43.      * @param {SudokuPuzzle} sudoku Instance of sudoku puzzle the cell is a part of
  44.      * @param {number} index The cell's index in the sudoku puzzle
  45.      */
  46.     constructor(sudoku, index) {
  47.         this.sudoku = sudoku;
  48.         this.index = index;
  49.         this.candidates = Array(sudoku.groupLength).fill().map((value, index) => index);
  50.         this.solved = false;
  51.     }
  52.  
  53.     /**Removes a candidate from the cell's candidate list
  54.      * @param {number} candidate The candidate value to remove
  55.      * @returns {boolean} true if the candidate was removed, false if not
  56.      * @throws {Error} Removing the candidate would leave the cell without a possible solution
  57.     */
  58.     exclude(candidate) {
  59.         if (this.solved) {
  60.             return false;
  61.         }
  62.  
  63.         // find the specified candidate in the cell's candidates list
  64.         let idx = this.candidates.findIndex(value => value === candidate);
  65.  
  66.         // candidate not found
  67.         if (idx === -1) {
  68.             return false;
  69.         }
  70.  
  71.         // no candidates would be left for the cells
  72.         if (this.candidates.length === 1) {
  73.             throw new Error('No candidates would be left');
  74.         }
  75.  
  76.         // remove the candidate
  77.         this.candidates.splice(idx, 1);
  78.  
  79.         // return true indicating the cell was changed
  80.         return true;
  81.     }
  82.  
  83.     /**Sets the cell's state to solved using the specified value
  84.      * @param {number} solution The value to use as the cell's solution
  85.      * @throws {Error} The cell has already been solved
  86.      * @throws {Error} The specified solution is not in the cell's candidates list
  87.     */
  88.     solveTo(solution) {
  89.         if (this.solved) {
  90.             throw new Error('cell already solved');
  91.         }
  92.  
  93.         if (this.candidates.indexOf(solution) === -1) {
  94.             throw new Error('given solution for cell is not valid');
  95.         }
  96.  
  97.         // Update cell
  98.         this.value = solution;
  99.         this.solved = true;
  100.         this.candidates = [];
  101.  
  102.         // Remove the solution from candidate lists of cells that share this row column or sector
  103.         this.row.exclude(solution);
  104.         this.column.exclude(solution);
  105.         this.sector.exclude(solution);
  106.     }
  107. }
  108.  
  109.  
  110.  
  111. /**Wrapper for rows, columns and sectors in a sudoku puzzle instance
  112.  * @private
  113.  * @property {SudokuPuzzle} sudoku The sudoku puzzle instance the group belongs to
  114.  * @property {number} index The index of the group
  115.  * @property {SudokuCell[]} cells List of cells in the group
  116.  * @property {number[]} candidates List of candidates left for the group
  117.  * @property {boolean} solved true if all cells in the group have been solved
  118.  */
  119. class SudokuCellGroup {
  120.  
  121.     /** Creates a group instance
  122.      * @param {'row'|'column'|'sector'} type The group type
  123.      * @param {SudokuPuzzle} sudoku The sudoku puzzle instance the group belongs to
  124.      * @param {number} index The index of the group
  125.      * @param {SudokuCell[]} cells The cells belonging to the group
  126.     */
  127.     constructor(type, sudoku, index, cells) {
  128.         this.sudoku = sudoku;
  129.         this.index = index;
  130.         this.candidates = Array(sudoku.groupLength).fill().map((value, index) => index);
  131.         this.solved = false;
  132.  
  133.         // loop over each cell that is a part of the group and add a reference to the group on the cell
  134.         let self = this;
  135.         cells.forEach(() => cells[type] = self);
  136.     }
  137.  
  138.     /**Removes a candidate from all cells in the group aswell as removing it from the group's candidate list
  139.      * @param {number} candidate The candidate to remove
  140.      * @returns {boolean} true if the puzzle was updated, false otherwise
  141.      */
  142.     exclude(candidate) {
  143.  
  144.         // all cells in group have been solved
  145.         if (this.solved) {
  146.             return false;
  147.         }
  148.  
  149.         // attempt to find the index of the candidate in the group's candidates list
  150.         let idx = this.candidates.indexOf(candidate);
  151.  
  152.         // candidate not found
  153.         if (idx === -1) {
  154.             return false;
  155.         }
  156.  
  157.         // exclude the candidate from all applicable cells in the group
  158.         this.cells.forEach(cell => cell.exclude(candidate));
  159.  
  160.         // remove the candidate from the group's candidate list
  161.         this.candidates = this.candidates.splice(idx, 1);
  162.  
  163.         // return true indicating the puzzle updated
  164.         return true;
  165.     }
  166. }
  167.  
  168.  
  169.  
  170. /**Creates sudoku puzzle instance
  171.  * @public
  172.  * @property {number} groupLength Number of cells within a group
  173.  * @property {Object} sectorSize Contains the width and height of a sector
  174.  * @property {number} sectorWidth Number of cells wide a sector is
  175.  * @property {number} sectorHeight Number of cells tall a sector is
  176.  * @property {SudokuCell[]} cells List of cell instances comprising the puzzle
  177.  * @property {SudokuCellGroup[]} row List of rows comprising the puzzle
  178.  * @property {SudokuCellGroup[]} column List of rows comprising the puzzle
  179.  * @property {SudokuCellGroup[]} sector List of rows comprising the puzzle
  180.  * @property {boolean} solved true if the puzzle has been solved, false otherwise
  181.  */
  182. class SudokuPuzzle {
  183.  
  184.     /** Creates a sudoku puzzle instance
  185.      * @param {number[]} puzzle A list of cell values for the puzzle where each item corrisponds to a cell
  186.      * @param {number|Object} [sectorSize] The size of each sector; if number, it will be used as both the width and height of the sector
  187.      * @param {number} [sectorSize.width] The width of the sector
  188.      * @param {number} [sectorSize.height] The width of the sector
  189.     */
  190.     constructor(puzzle, sectorSize) {
  191.  
  192.         // Puzzle not specified
  193.         if (!Array.isArray(puzzle)) {
  194.             throw new Error('INVALID_PUZZLE: puzzle must be an 1-dimensional array');
  195.         }
  196.  
  197.         // Puzzle must be a square
  198.         let groupLength = Math.sqrt(puzzle.length);
  199.         if (!isUINT(groupLength)) {
  200.             throw new Error('INVALID_PUZZLE: puzzle must be a square');
  201.         }
  202.  
  203.         // Every cell value in the puzzle must be an unsigned integer no larger than the length of a row|column|sector
  204.         if (!puzzle.every(value => isUINT(value, {max: groupLength}))) {
  205.             throw new Error('PUZZLE_INVALID: puzzle must only contain unsigned integer values');
  206.         }
  207.  
  208.         // sector size not specified
  209.         if (!sectorSize || (!sectorSize.width && !sectorSize.height)) {
  210.             sectorSize = Math.sqrt(groupLength);
  211.         }
  212.  
  213.         // sector size specified as an integer
  214.         if (isUINT(sectorSize))  {
  215.             sectorSize = {width: sectorSize, height: sectorSize};
  216.         }
  217.  
  218.         // sector size specified as an object
  219.         if (sectorSize.width || sectorSize.height) {
  220.  
  221.             // only one dimension of the sector specified
  222.             if (!sectorSize.width) {
  223.                 sectorSize.width = sectorSize.height;
  224.             } else if (!sectorSize.height) {
  225.                 sectorSize.height = sectorSize.width;
  226.             }
  227.  
  228.             // sector width|height not an unsigned integer
  229.             if (!isUINT(sectorSize.width)) {
  230.                 throw new Error('PUZZLE_INVALID: sector width not an unsigned integer');
  231.             }
  232.             if (!isUINT(sectorSize.height)) {
  233.                 throw new Error('PUZZLE_INVALID: sector height not an unsigned integer');
  234.             }
  235.  
  236.             // sector would not evenly divide the puzzle
  237.             if (sectorSize.width * sectorSize.height !== groupLength) {
  238.                 throw new Error('PUZZLE_INVALID: sector dimensions do not divide the puzzle evenly');
  239.             }
  240.  
  241.         } else {
  242.             throw new Error('PUZZLE_INVALID: invalid sector size');
  243.         }
  244.  
  245.         let sudoku = this;
  246.  
  247.         // store group length, sector width and sector height
  248.         sudoku.groupLength  = groupLength;
  249.         sudoku.sectorWidth  = sectorSize.width;
  250.         sudoku.sectorHeight = sectorSize.height;
  251.  
  252.         // create a list of all cells in the puzzle
  253.         sudoku.cells = Array(puzzle.length).fill().map((ignore, index) => {
  254.             return new SudokuCell(sudoku, index, puzzle[index]);
  255.         });
  256.  
  257.         // create a list of rows in the puzzle with references to their respective cells
  258.         sudoku.rows = Array(groupLength).fill().map((ignore, rowIndex) => {
  259.             return new SudokuCellGroup(
  260.                 'row',
  261.                 sudoku,
  262.                 rowIndex,
  263.                 self.cells.slice(rowIndex * groupLength, (rowIndex + 1) * groupLength)
  264.             );
  265.         });
  266.  
  267.         // create a list of columns in the puzzle with references to their respective cells
  268.         self.columns = Array(groupLength).fill().map((ignore, columnIndex) => {
  269.             return new SudokuCellGroup(
  270.                 'column',
  271.                 sudoku,
  272.                 columnIndex,
  273.                 Array(groupLength).fill().map((ignore, index) => self.cells[groupLength * index])
  274.             );
  275.         });
  276.  
  277.         sudoku.sectors = Array(groupLength).fill().map((ignore, sectorIndex) => {
  278.  
  279.             let cells = [],
  280.  
  281.                 // Calculate the index of the first row of which enters the sector
  282.                 rowIndex = Math.floor(sectorIndex / sudoku.sectorHeight) * sudoku.sectorHeight,
  283.  
  284.                 // Calculate the index of which cells start for the sector within a row
  285.                 cellStart = sectorIndex * sudoku.sectorWidth % groupLength,
  286.  
  287.                 // Calculate the index of which cells end for the sector within a row
  288.                 cellEnd = cellStart + sudoku.sectorWidth;
  289.  
  290.             // loop over each row starting at rowIndex and ending just before rowIndex + sector_height
  291.             for (let rowOffset = 0; rowOffset < sudoku.sectorHeight; rowOffset += 1) {
  292.  
  293.                 // get the list of cells from the row that belong to this sector and append them to the sectors cell list
  294.                 cells.push(...sudoku.rows[rowIndex + rowOffset].slice(cellStart, cellEnd));
  295.             }
  296.  
  297.             // create a cell group instance for the sector
  298.             return new SudokuCellGroup('sector', sudoku, sectorIndex, cells);
  299.         });
  300.  
  301.         // load the specified puzzle's values
  302.         puzzle.forEach((value, index) => {
  303.             if (value) {
  304.                 sudoku.cells[index].solveTo(value);
  305.             }
  306.         });
  307.     }
  308.  
  309.     get solved() {
  310.         return this.cells.reduce((solved, cell) => solved && cell.solved, true);
  311.     }
  312. }
  313.  
  314. /**@exports {SudokuPuzzle} */
  315. module.exports = SudokuPuzzle;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement