├── .gitignore ├── README.md ├── board.js ├── builder.js ├── example.js ├── index.js ├── package.json ├── solver.js ├── tests ├── test1.js ├── test2.js └── test3.js └── web ├── index.html ├── main.js ├── nonogram.css └── nonogram.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | tmp 4 | docs 5 | npm-debug.log 6 | bower_components 7 | dist 8 | Node_Framework.sublime-project 9 | Node_Framework.sublime-workspace 10 | project.sublime-project 11 | project.sublime-workspace 12 | /test*.js 13 | *.swp 14 | web/bundle.js 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project contains an advanced nonogram puzzle solver and generator. See example.js for usage. 2 | 3 | -------------------------------------------------------------------------------- /board.js: -------------------------------------------------------------------------------- 1 | const deepCopy = require('objtools').deepCopy; 2 | 3 | /** 4 | * This class represents a puzzle board and includes the dimensions (rows and columns), 5 | * clues, and cell data. Cell data may or may not include unknowns. Also supported 6 | * are color nonograms, where each cell can contain colors other than black. 7 | * 8 | * Board data is accessible at the `data` property and is a single-dimensional array 9 | * of data. Accessors should be used to access elements. 10 | * 11 | * Dimensions are available as the `rows` and `cols` properties. 12 | * 13 | * Clues are accessible at the `rowClues` and `colClues` properties. These are arrays 14 | * such that the array index corresponds to the number of row or column. 0,0 is at 15 | * the upper-left of the puzzle. This array contains arrays of clue objects, where 16 | * each clue object looks like `{ value: 1, run: 3 }`. `value` represents the color of 17 | * the clue, and is always 1 for black-and-white puzzles. `run` is the count of the clue. 18 | * 19 | * Values in the data (and the clues) are represented as numbers. 0 is 'blank', and 1+ 20 | * are cell colors. The special value `null` can be used in the data (but not in the clues) 21 | * to represent an unknown cell. 22 | * 23 | * @class Board 24 | * @constructor 25 | * @param {Number} rows - Number of rows, ie, height 26 | * @param {Number} cols - Number of columns, ie, width 27 | */ 28 | class Board { 29 | 30 | constructor(rows, cols) { 31 | if (rows < 2) rows = 2; 32 | if (cols < 2) cols = 2; 33 | this.rows = rows; 34 | this.cols = cols; 35 | this.clearData(0); 36 | this.rowClues = []; 37 | this.colClues = []; 38 | for (let i = 0; i < this.rows; i++) this.rowClues.push([]); 39 | for (let i = 0; i < this.cols; i++) this.colClues.push([]); 40 | } 41 | 42 | /** 43 | * Resizes the board 44 | * 45 | * @method resize 46 | * @param {Number} newRows 47 | * @param {Number} newCols 48 | * @param {Number} defaultValue 49 | */ 50 | resize(newRows, newCols, defaultValue = 0) { 51 | if (newRows < 2) newRows = 2; 52 | if (newCols < 2) newCols = 2; 53 | let haveUnknowns = false; 54 | let newData = []; 55 | for (let row = 0; row < newRows; row++) { 56 | for (let col = 0; col < newCols; col++) { 57 | let value; 58 | if (col < this.cols && row < this.rows) { 59 | value = this.get(row, col); 60 | } else { 61 | value = defaultValue; 62 | } 63 | newData[row * newCols + col] = value; 64 | if (value === null) haveUnknowns = true; 65 | } 66 | } 67 | this.rows = newRows; 68 | this.cols = newCols; 69 | this.data = newData; 70 | if (!haveUnknowns) { 71 | this.buildCluesFromData(); 72 | } else { 73 | this.rowClues.length = this.rows; 74 | this.colClues.length = this.cols; 75 | for (let row = 0; row < this.rows; row++) { 76 | if (!this.rowClues[row]) this.rowClues[row] = []; 77 | } 78 | for (let col = 0; col < this.cols; col++) { 79 | if (!this.colClues[col]) this.colClues[col] = []; 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Method to generate random board data. 86 | * 87 | * @method makeRandomBoard 88 | * @static 89 | * @param {Number} rows - Number of rows 90 | * @param {Number} cols - Number of columns 91 | * @param {Number} [values=1] - Number of values, 1 for black-and-white 92 | * @param {Number} [density=null] - Density of filled-in cells. Default is to pick at random between 0.2 and 0.8 . 93 | * @return {Board} 94 | */ 95 | static makeRandomBoard(rows, cols, values = 1, density = null) { 96 | if (density === null) density = Math.random() * 0.6 + 0.2; 97 | let board = new Board(rows, cols); 98 | for (let i = 0; i < board.data.length; i++) { 99 | if (Math.random() < density) { 100 | board.data[i] = Math.floor(Math.random() * values) + 1; 101 | } else { 102 | board.data[i] = 0; 103 | } 104 | } 105 | board.buildCluesFromData(); 106 | return board; 107 | } 108 | 109 | /** 110 | * Fills the board data with all of the same value. 111 | * 112 | * @method clearData 113 | * @param {Number} [value=null] - Value to set 114 | */ 115 | clearData(value = null) { 116 | this.data = []; 117 | for (let i = 0; i < this.rows * this.cols; i++) this.data.push(value); 118 | } 119 | 120 | /** 121 | * Creates a string token that uniquely represents the full state of board data. 122 | * 123 | * @method makeToken 124 | */ 125 | makeToken() { 126 | function valToken(val) { 127 | if (val === null) return 'x'; 128 | if (Array.isArray(val)) { 129 | return val.join('|'); 130 | } 131 | return '' + val; 132 | } 133 | return this.data.map(valToken).join(','); 134 | } 135 | 136 | /** 137 | * Take the board data and compute the board clues from it. 138 | * 139 | * @method buildCluesFromData 140 | */ 141 | buildCluesFromData() { 142 | let { rowClues, colClues } = this._makeCluesFromData(false); 143 | this.rowClues = rowClues; 144 | this.colClues = colClues; 145 | } 146 | 147 | _makeCluesFromData(includeBlanks = false, countUnknownAsBlank = false) { 148 | let rowClues = []; 149 | for (let row = 0; row < this.rows; row++) { 150 | let thisRowClues = []; 151 | let lastValue = this.get(row, 0); 152 | if (lastValue === null && countUnknownAsBlank) lastValue = 0; 153 | let startOfRun = 0; 154 | for (let col = 1; col <= this.cols; col++) { 155 | let value = (col === this.cols) ? -1 : this.get(row, col); 156 | if (value === null && countUnknownAsBlank) value = 0; 157 | if (value !== lastValue || col === this.cols) { 158 | if (typeof lastValue !== 'number') throw new Error('Cannot build clues from unknown grid'); 159 | let runLength = col - startOfRun; 160 | if (lastValue !== 0 || includeBlanks) { 161 | thisRowClues.push({ value: lastValue, run: runLength }); 162 | } 163 | lastValue = value; 164 | startOfRun = col; 165 | } 166 | } 167 | rowClues.push(thisRowClues); 168 | } 169 | 170 | let colClues = []; 171 | for (let col = 0; col < this.cols; col++) { 172 | let thisColClues = []; 173 | let lastValue = this.get(0, col); 174 | if (lastValue === null && countUnknownAsBlank) lastValue = 0; 175 | let startOfRun = 0; 176 | for (let row = 1; row <= this.rows; row++) { 177 | let value = (row === this.rows) ? -1 : this.get(row, col); 178 | if (value === null && countUnknownAsBlank) value = 0; 179 | if (value !== lastValue || row === this.rows) { 180 | let runLength = row - startOfRun; 181 | if (lastValue !== 0 || includeBlanks) { 182 | thisColClues.push({ value: lastValue, run: runLength }); 183 | } 184 | lastValue = value; 185 | startOfRun = row; 186 | } 187 | } 188 | colClues.push(thisColClues); 189 | } 190 | 191 | return { rowClues, colClues }; 192 | } 193 | 194 | /** 195 | * Returns true if there are no unknowns 196 | * 197 | * @method isComplete 198 | * @return {Boolean} 199 | */ 200 | isComplete() { 201 | for (let value of this.data) { 202 | if (value === null) return false; 203 | } 204 | return true; 205 | } 206 | 207 | /** 208 | * Checks for a valid solution. Returns true if valid. 209 | * 210 | * @method validate 211 | * @return {Boolean} 212 | */ 213 | validate(countUnknownAsBlank = false) { 214 | let { rowClues, colClues } = this._makeCluesFromData(false, countUnknownAsBlank); 215 | for (let row = 0; row < this.rows; row++) { 216 | if (rowClues[row].length !== this.rowClues[row].length) return false; 217 | for (let i = 0; i < rowClues[row].length; i++) { 218 | if ( 219 | rowClues[row][i].value !== this.rowClues[row][i].value || 220 | rowClues[row][i].run !== this.rowClues[row][i].run 221 | ) return false; 222 | } 223 | } 224 | for (let col = 0; col < this.cols; col++) { 225 | if (colClues[col].length !== this.colClues[col].length) return false; 226 | for (let i = 0; i < colClues[col].length; i++) { 227 | if ( 228 | colClues[col][i].value !== this.colClues[col][i].value || 229 | colClues[col][i].run !== this.colClues[col][i].run 230 | ) return false; 231 | } 232 | } 233 | return true; 234 | } 235 | 236 | // 0 is blank, 1+ are colors, null is unknown 237 | get(row, col) { 238 | if (row >= this.rows || col >= this.cols) throw new Error('Out of bounds'); 239 | return deepCopy(this.data[row * this.cols + col]); 240 | } 241 | 242 | set(row, col, value) { 243 | if (row >= this.rows || col >= this.cols) throw new Error('Out of bounds'); 244 | this.data[row * this.cols + col] = deepCopy(value); 245 | } 246 | 247 | getRow(row) { 248 | let ar = Array(this.cols); 249 | for (let i = 0; i < this.cols; i++) ar[i] = this.get(row, i); 250 | return ar; 251 | } 252 | 253 | getCol(col) { 254 | let ar = Array(this.rows); 255 | for (let i = 0; i < this.rows; i++) ar[i] = this.get(i, col); 256 | return ar; 257 | } 258 | 259 | setRow(row, line) { 260 | for (let i = 0; i < this.cols; i++) this.set(row, i, line[i]); 261 | } 262 | 263 | setCol(col, line) { 264 | for (let i = 0; i < this.rows; i++) this.set(i, col, line[i]); 265 | } 266 | 267 | /** 268 | * Computes and returns the maximum value present across clues and data. 269 | * 270 | * @method getMaxValue() 271 | * @return {Number} 272 | */ 273 | getMaxValue() { 274 | let maxValue = 0; 275 | for (let i = 0; i < this.data.length; i++) { 276 | if (typeof this.data[i] === 'number' && this.data[i] > maxValue) maxValue = this.data[i]; 277 | else if (Array.isArray(this.data[i])) { 278 | for (let possibleValue of this.data[i]) { 279 | if (possibleValue > maxValue) maxValue = possibleValue; 280 | } 281 | } 282 | } 283 | for (let clues of [ this.rowClues, this.colClues ]) { 284 | for (let rcClues of clues) { 285 | for (let clue of rcClues) { 286 | if (clue.value > maxValue) maxValue = clue.value; 287 | } 288 | } 289 | } 290 | return maxValue; 291 | } 292 | 293 | /** 294 | * Prints to the console a textual representation of this board. 295 | * 296 | * @method printBoard 297 | * @param {String} [blankStr='X'] - Character to use for blank cells 298 | * @param {String} [unknownStr=' '] - Character to use for unknown cells 299 | */ 300 | printBoard(blankStr = 'X', unknownStr = ' ') { 301 | let maxValue = this.getMaxValue(); 302 | 303 | function clueStr(clue) { 304 | if (maxValue > 1) { 305 | return `${clue.run}(${clue.value})`; 306 | } else { 307 | return '' + clue.run; 308 | } 309 | } 310 | 311 | function valStr(value) { 312 | if (typeof value !== 'number') return unknownStr; 313 | if (value === 0) return blankStr; 314 | return '' + value; 315 | } 316 | 317 | function padStr(str, len, padStyle = 'left') { 318 | let padding = ''; 319 | let padLen = len - str.length; 320 | for (let i = 0; i < padLen; i++) { 321 | padding += ' '; 322 | } 323 | if (padStyle === 'left') { 324 | return padding + str; 325 | } else if (padStyle === 'right') { 326 | return str + padding; 327 | } else { 328 | padding = padding.slice(0, Math.floor(padLen / 2)); 329 | return padStr(str + padding, len, 'left'); 330 | } 331 | } 332 | 333 | function padTo(value, len) { 334 | return padStr(valStr(value), len, 'center'); 335 | } 336 | 337 | let maxCellLength = 1; 338 | for (let i = 0; i < this.data.length; i++) { 339 | let str = valStr(this.data[i]); 340 | if (str.length > maxCellLength) maxCellLength = str.length; 341 | } 342 | for (let col = 0; col < this.colClues.length; col++) { 343 | for (let i = 0; i < this.colClues[col].length; i++) { 344 | let str = clueStr(this.colClues[col][i]); 345 | if (str.length > maxCellLength) maxCellLength = str.length; 346 | } 347 | } 348 | 349 | let maxNumColClues = 0; 350 | for (let i = 0; i < this.colClues.length; i++) { 351 | if (this.colClues[i].length > maxNumColClues) maxNumColClues = this.colClues[i].length; 352 | } 353 | 354 | const rowSeparator = '-'; 355 | const colSeparator = ' | '; 356 | const rowClueSpacing = ' '; 357 | const clueColSeparator = ' | '; 358 | const clueRowSeparator = '+'; 359 | 360 | // Generate row clues 361 | let rowClueStrs = []; 362 | let maxRowClueStrLen = 0; 363 | for (let row = 0; row < this.rowClues.length; row++) { 364 | let thisRowClues = this.rowClues[row]; 365 | let thisRowClueStr = ''; 366 | for (let i = 0; i < thisRowClues.length; i++) { 367 | if (i > 0) thisRowClueStr += rowClueSpacing; 368 | thisRowClueStr += clueStr(thisRowClues[i]); 369 | } 370 | if (thisRowClueStr.length > maxRowClueStrLen) maxRowClueStrLen = thisRowClueStr.length; 371 | rowClueStrs.push(thisRowClueStr); 372 | } 373 | // Pad rowClueStrs and add separator 374 | for (let row = 0; row < rowClueStrs.length; row++) { 375 | rowClueStrs[row] = padStr(rowClueStrs[row], maxRowClueStrLen) + clueColSeparator; 376 | } 377 | 378 | const printRowSeparatorLine = (c) => { 379 | let str = ''; 380 | let len = maxRowClueStrLen + this.cols * (maxCellLength + colSeparator.length); 381 | for (let i = 0; i < len; i++) str += c; 382 | console.log(str); 383 | }; 384 | 385 | // Print column clue rows 386 | for (let colClueRowNum = 0; colClueRowNum < maxNumColClues; colClueRowNum++) { 387 | let colClueRowStr = padStr('', maxRowClueStrLen) + colSeparator; 388 | for (let col = 0; col < this.colClues.length; col++) { 389 | if (col !== 0) colClueRowStr += colSeparator; 390 | let thisColClues = this.colClues[col]; 391 | if (colClueRowNum < maxNumColClues - thisColClues.length) { 392 | colClueRowStr += padStr('', maxCellLength); 393 | } else { 394 | let clue = thisColClues[colClueRowNum - (maxNumColClues - thisColClues.length)]; 395 | colClueRowStr += padStr(clueStr(clue), maxCellLength); 396 | } 397 | } 398 | console.log(colClueRowStr); 399 | } 400 | 401 | // Print data rows 402 | for (let row = 0; row < this.rows; row++) { 403 | printRowSeparatorLine(row === 0 ? clueRowSeparator : rowSeparator); 404 | let rowStr = ''; 405 | rowStr += rowClueStrs[row]; 406 | for (let col = 0; col < this.cols; col++) { 407 | if (col > 0) rowStr += colSeparator; 408 | rowStr += padTo(this.get(row, col), maxCellLength); 409 | } 410 | console.log(rowStr); 411 | } 412 | } 413 | 414 | serialize() { 415 | function serializeClues(lineClues) { 416 | return lineClues.map((clues) => { 417 | return clues.map((clue) => { 418 | return `${clue.value}x${clue.run}`; 419 | }).join('.'); 420 | }).join(','); 421 | } 422 | return [ 423 | this.rows, 424 | this.cols, 425 | serializeClues(this.rowClues), 426 | serializeClues(this.colClues), 427 | this.data.map((val) => { 428 | return val === null ? 'x' : val; 429 | }).join(',') 430 | ].join('|'); 431 | } 432 | 433 | static deserialize(str) { 434 | function deserializeClues(str) { 435 | let lines = str.split(','); 436 | return lines.map((lineStr) => { 437 | if (lineStr === '') return []; 438 | let clues = lineStr.split('.'); 439 | return clues.map((clueStr) => { 440 | let clueParts = clueStr.split('x'); 441 | return { value: parseInt(clueParts[0]), run: parseInt(clueParts[1]) }; 442 | }); 443 | }); 444 | } 445 | let parts = str.split('|'); 446 | let rows = parseInt(parts[0]); 447 | let cols = parseInt(parts[1]); 448 | let board = new Board(rows, cols); 449 | board.rowClues = deserializeClues(parts[2]); 450 | board.colClues = deserializeClues(parts[3]); 451 | board.data = parts[4].split(',').map((str) => { 452 | return (str === 'x') ? null : parseInt(str); 453 | }); 454 | return board; 455 | } 456 | 457 | } 458 | 459 | module.exports = Board; 460 | 461 | 462 | -------------------------------------------------------------------------------- /builder.js: -------------------------------------------------------------------------------- 1 | const deepCopy = require('objtools').deepCopy; 2 | const Solver = require('./solver'); 3 | 4 | /** 5 | * This class contains the logic for building a puzzle, given a desired set of data. 6 | * 7 | * This builder does more than just computing clues. It repeatedly attempts to solve 8 | * puzzles using methods similar to what a human might use, and attempts to construct 9 | * a puzzle to given difficulty parameters. It uses these parameters to determine 10 | * which cells should be prefilled for an optimal difficulty. It also ensures 11 | * solution uniqueness. 12 | * 13 | * Because of the method used, the builder can sometimes take time to run. Limits 14 | * can be put on the builder's runtime using some of the parameters (those beginning with "max"). 15 | * 16 | * Available parameters are: 17 | * - maxSolverBranches - Max number of allowed branches for the recursive solver. If this is set 18 | * to 0, only puzzles that can be solved analytically row-by-row are generated. Nonzero values 19 | * indicate the total number of logical "branches" that must be taken when experimenting/brute 20 | * forcing values, including dead ends. This corresponds to an effective worst-case number 21 | * of branches a human might have to take when solving. 22 | * - maxDeadEndDepth - For non-solution branches (ie, dead-ends), the maximum number of branches that 23 | * might have to be followed inside of that dead end. Set to 0 to enforce dead ends to be 24 | * determinable using row-by-row logic. 25 | * - maxDeadEndSteps - The maximum number of row-by-row logical steps required to discover that a 26 | * branch is a dead-end. 27 | * - maxTotalSteps - Worst case total number of row-by-row logical steps required to solve the 28 | * puzzle, including following every dead end path. 29 | * - maxSolutionDepth - The number of forks along the solution path, excepting row-by-row logic. 30 | * This is 0 for purely row-by-row puzzles. 31 | * - targetDeadEnds - Desired number of dead ends in the puzzle 32 | * - targetTotalSteps - Target number of total steps followed 33 | * - targetSolutionDepth - Target depth of solution 34 | * - numPuzzleIterations - The builder will try this number of random puzzle configurations, and 35 | * pick the one that most optimally fits the targets. This can be set to 1 to use the first 36 | * random puzzle selected. 37 | * - simpleSolveCrossLines - Whether to allow the simple solver to take into account partially 38 | * known cells. 39 | * 40 | * @class Builder 41 | * @constructor 42 | * @param {Board} board - Board containing data (no unknowns). 43 | * @param {Object} [params] 44 | */ 45 | class Builder { 46 | 47 | constructor(board, params = {}) { 48 | this.filledBoard = board; 49 | board.buildCluesFromData(); 50 | this.setBuilderParams(params); 51 | } 52 | 53 | /** 54 | * This is the main accessor method of the class. Given a board with filled-in data, and 55 | * a difficulty level 1-10, it generates and returns a puzzle board with unknown cells. 56 | * 57 | * The returned object contains: 58 | * - board - The puzzle board 59 | * - stats - An object containing various statistics on solution difficulty 60 | * - score - A score of how closely the board fits the parameters; lower is better 61 | * 62 | * @method buildPuzzleFromData 63 | * @param {Board} board - The board containing data to generate a puzzle from 64 | * @param {Number} level - The difficulty level from 1 to 10 65 | * @return {Object} 66 | */ 67 | static buildPuzzleFromData(board, level = 3) { 68 | let builder = new Builder(board, Builder.makeParamsFromDifficulty(level)); 69 | return builder.buildPuzzle(); 70 | } 71 | 72 | setBuilderParams(params = {}) { 73 | let defaultParams = { 74 | // Maximum number of times the solver can "branch" when the puzzle can't be simple-solved. 75 | // Set to 0 to only allow simple-solve puzzles. 76 | maxSolverBranches: 10, 77 | // The maximum recursive depth of non-solution paths. A value of 0 indicates that all dead-end paths must be determinable by simple solve 78 | maxDeadEndDepth: 0, 79 | // The maximum number of simple-solve steps allowed for a dead-end path 80 | maxDeadEndSteps: 3, 81 | // Max total number of simple solve steps, both for finding solution and for invalidating dead-end paths 82 | maxTotalSteps: 1000, 83 | // Max number of branches deep the solution can be; this corresponds to how many times the human solver would get "stuck" solving analytically 84 | maxSolutionDepth: 3, 85 | // Try to hit this solution depth 86 | targetSolutionDepth: 3, 87 | targetDeadEnds: 2, 88 | targetTotalSteps: 300, 89 | numPuzzleIterations: 10, 90 | simpleSolveCrossLines: true 91 | }; 92 | for (let key in defaultParams) { 93 | if (params[key] === undefined || params[key] === null) params[key] = defaultParams[key]; 94 | } 95 | this.params = params; 96 | } 97 | 98 | /** 99 | * Returns the params object associated with a given difficulty level from 1-10. 100 | * 101 | * @method makeParamsFromDifficulty 102 | * @param {Number} level 103 | * @return {Object} 104 | */ 105 | static makeParamsFromDifficulty(level) { 106 | if (level < 1) level = 1; 107 | if (level < 3) { 108 | return { 109 | maxSolverBranches: 0, 110 | maxDeadEndDepth: 0, 111 | maxDeadEndSteps: 0, 112 | maxTotalSteps: 100, 113 | maxSolutionDepth: 0, 114 | targetSolutionDepth: 0, 115 | targetDeadEnds: 0, 116 | targetTotalSteps: level * 10, 117 | numPuzzleIterations: 2, 118 | simpleSolveCrossLines: false 119 | }; 120 | } else if (level < 5) { 121 | return { 122 | maxSolverBranches: 0, 123 | maxDeadEndDepth: 0, 124 | maxDeadEndSteps: 0, 125 | maxTotalSteps: 1000, 126 | maxSolutionDepth: 0, 127 | targetSolutionDepth: 0, 128 | targetDeadEnds: 0, 129 | targetTotalSteps: (level - 2) * 100, 130 | numPuzzleIterations: 2, 131 | simpleSolveCrossLines: true 132 | }; 133 | } else { 134 | return { 135 | maxSolverBranches: 10 * (level - 4), 136 | maxDeadEndDepth: (level < 8) ? 0 : (level - 7), 137 | maxDeadEndSteps: (level < 8) ? ((level - 4) * 2) : ((level - 7) * 20), 138 | maxTotalSteps: 10000 * level, 139 | maxSolutionDepth: level - 2, 140 | targetSolutionDepth: level - 4, 141 | targetDeadEnds: (level - 4) * 2, 142 | targetTotalSteps: level * 100, 143 | numPuzzleIterations: 2, 144 | simpleSolveCrossLines: true 145 | }; 146 | } 147 | } 148 | 149 | /** 150 | * Checks to see if this board is solveable, and returns stats on the solution. 151 | * Returns object with solver stats if can be solved, or null if can't be solved within parameters to a unique solution. 152 | * Bails out early if the maximums specified in the parameters are hit. 153 | * 154 | * @method _trySolve 155 | * @private 156 | * @param {Board} 157 | * @return {Object} 158 | */ 159 | _trySolve(board) { 160 | // The basic structure of this method is similar to the corresponding solver method 161 | 162 | // Whether a solution has been found. Used to check for multiple solutions. 163 | let foundSolution = false; 164 | let maxValue = board.getMaxValue(); 165 | let visitedSet = {}; 166 | 167 | let allPossibleValues = []; 168 | for (let i = 0; i <= maxValue; i++) allPossibleValues.push(i); 169 | 170 | let numBranches = 0; 171 | let curTotalSteps = 0; 172 | 173 | // This recursive function returns stats about the current "branch", including: 174 | // - depth the branch reached 175 | // - The number of simple solve steps used in the branch (total steps for both exhausting all dead ends and solving) 176 | // - Whether or not the branch ended in a solution 177 | const findSolutionsFromState = (solver) => { 178 | 179 | // Make all simple-solve steps possible 180 | let simpleSolveResult = solver.simpleSolveBatch(this.params.simpleSolveCrossLines); 181 | 182 | // Check if this branch is a dead end 183 | if (simpleSolveResult.contradiction) { 184 | return { 185 | maxDepth: 0, 186 | steps: simpleSolveResult.steps, 187 | deadEnds: 1, 188 | solution: false 189 | }; 190 | } 191 | 192 | // Check if this branch has been visited before 193 | let token = solver.board.makeToken(); 194 | if (visitedSet[token]) return visitedSet[token]; 195 | 196 | // Early bail if hit max total steps 197 | curTotalSteps += simpleSolveResult.steps; 198 | if (curTotalSteps > this.params.maxTotalSteps) throw new Error('hit max total steps'); 199 | 200 | // If the solution is complete ... 201 | if (simpleSolveResult.remainingUnknowns === 0) { 202 | // Make sure solution matches the desired one 203 | let mismatchedIndexes = []; 204 | for (let i = 0; i < solver.board.data.length; i++) { 205 | if (solver.board.data[i] !== this.filledBoard.data[i]) { 206 | mismatchedIndexes.push(i); 207 | } 208 | } 209 | if (mismatchedIndexes.length) { 210 | let err = new Error('wrong/ambiguous solution'); 211 | err.mismatchedIndexes = mismatchedIndexes; 212 | throw err; 213 | } 214 | if (foundSolution) throw new Error('non-unique solution'); 215 | foundSolution = true; 216 | let result = { 217 | maxDepth: 0, 218 | steps: simpleSolveResult.steps, 219 | deadEnds: 0, 220 | solution: true, 221 | solutionDepth: 0 222 | }; 223 | visitedSet[token] = deepCopy(result); 224 | visitedSet[token].cached = true; 225 | return result; 226 | } 227 | 228 | numBranches++; 229 | if (numBranches > this.params.maxSolverBranches) throw new Error('hit max branches'); 230 | 231 | let itMaxDepth = 0; 232 | let itSolveSteps = simpleSolveResult.steps; 233 | let itSolution = false; 234 | let itDeadEnds = 0; 235 | let itSolutionDepth; 236 | 237 | // Find first unknown 238 | let foundUnknown = false; 239 | for (let row = 0; row < solver.board.rows; row++) { 240 | for (let col = 0; col < solver.board.cols; col++) { 241 | let value = solver.board.get(row, col); 242 | if (value === null || Array.isArray(value)) { 243 | // Try it with this unknown being each of the possible values 244 | let possibleValues = (value === null) ? allPossibleValues : value; 245 | for (let possibleValue of possibleValues) { 246 | solver.board.set(row, col, possibleValue); 247 | let res = findSolutionsFromState(solver.partialDup()); 248 | if (res.solution) { 249 | itSolution = true; 250 | itSolutionDepth = res.solutionDepth; 251 | } 252 | if (res.maxDepth > itMaxDepth) itMaxDepth = res.maxDepth; 253 | if (!res.cached) itSolveSteps += res.steps; 254 | if (!res.solution) { 255 | // This path was a dead end 256 | if (res.maxDepth > this.params.maxDeadEndDepth) throw new Error('hit max dead end depth'); 257 | if (res.steps > this.params.maxDeadEndSteps) throw new Error('hit max dead end steps'); 258 | if (!res.cached) itDeadEnds++; 259 | } 260 | solver.board.set(row, col, value); 261 | } 262 | foundUnknown = true; 263 | break; 264 | } 265 | } 266 | if (foundUnknown) break; 267 | } 268 | 269 | let result = { 270 | maxDepth: itMaxDepth + 1, 271 | steps: itSolveSteps, 272 | solution: itSolution, 273 | deadEnds: itDeadEnds, 274 | solutionDepth: (itSolutionDepth === undefined) ? undefined : (itSolutionDepth + 1) 275 | }; 276 | visitedSet[token] = deepCopy(result); 277 | visitedSet[token].cached = true; 278 | return result; 279 | }; 280 | 281 | let res = findSolutionsFromState(new Solver(Solver.partialCopyBoard(board))); 282 | if (!res.solution) throw new Error('Tried to solve unsolvable puzzle'); 283 | if (res.solutionDepth > this.params.maxSolutionDepth) throw new Error('hit max solution depth'); 284 | if (res.steps > this.params.maxTotalSteps) throw new Error('hit max total steps'); 285 | 286 | return res; 287 | } 288 | 289 | /** 290 | * Starts filling in random cells of the puzzle, trying to solve it, and calling 291 | * a callback for each. 292 | * 293 | * @method _tryRandomPuzzle 294 | * @private 295 | * @param {Function} puzzleCb 296 | */ 297 | _tryRandomPuzzle(puzzleCb) { 298 | let board = Solver.partialCopyBoard(this.filledBoard); 299 | board.clearData(); 300 | // Fill in random unknown cells repeatedly 301 | for (;;) { 302 | let tryIndexes; 303 | let tsr; 304 | try { 305 | tsr = this._trySolve(board); 306 | } catch (ex) { 307 | if (ex.mismatchedIndexes) tryIndexes = ex.mismatchedIndexes; 308 | } 309 | if (tsr) { 310 | let cbRet = puzzleCb(Solver.partialCopyBoard(board), tsr); 311 | if (cbRet === false) break; 312 | } 313 | let simpleSolver = new Solver(Solver.partialCopyBoard(board)); 314 | let { remainingUnknowns, contradiction } = simpleSolver.simpleSolveBatch(); 315 | if (contradiction) throw new Error('got solver contradiction while building puzzle'); 316 | if (tryIndexes && Math.random() < 0.9) { 317 | let idx = tryIndexes[Math.floor(Math.random() * tryIndexes.length)]; 318 | board.data[idx] = this.filledBoard.data[idx]; 319 | } else if (remainingUnknowns > 1) { 320 | // Pick random unknown that can't be simple-solved to fill in 321 | let unknownNo = Math.floor(Math.random() * remainingUnknowns); 322 | let unknownCtr = 0; 323 | for (let i = 0; i < board.data.length; i++) { 324 | if (simpleSolver.board.data[i] === null) { 325 | if (unknownCtr === unknownNo) { 326 | board.data[i] = this.filledBoard.data[i]; 327 | break; 328 | } 329 | unknownCtr++; 330 | } 331 | } 332 | } else { 333 | // Pick a random unknown, regardless of if it can be simple-solved or not 334 | let numUnknowns = 0; 335 | for (let value of board.data) { 336 | if (value === null) numUnknowns++; 337 | } 338 | let unknownNo = Math.floor(Math.random() * numUnknowns); 339 | let unknownCtr = 0; 340 | for (let i = 0; i < board.data.length; i++) { 341 | if (board.data[i] === null) { 342 | if (unknownCtr === unknownNo) { 343 | board.data[i] = this.filledBoard.data[i]; 344 | break; 345 | } 346 | unknownCtr++; 347 | } 348 | } 349 | } 350 | // Make sure there's at least one unknown cell left 351 | let atLeastOneUnknown = false; 352 | for (let value of board.data) { 353 | if (value === null) atLeastOneUnknown = true; 354 | } 355 | if (!atLeastOneUnknown) break; 356 | } 357 | } 358 | 359 | /** 360 | * Generate a score for how close a puzzle solution is to the target stats. 361 | * 362 | * @method _scoreStats 363 | * @private 364 | * @param {Object} stats 365 | * @param {Board} board 366 | * @return {Number} 367 | */ 368 | _scoreStats(stats, board) { 369 | // compare stats.solutionDepth to targetSolutionDepth 370 | // compare stats.deadEnds to targetDeadEnds 371 | // compare stats.steps to targetTotalSteps 372 | let scoreMSE = 0; 373 | let countMSE = 0; 374 | function addToScore(stat, target, weight = 1) { 375 | let percentErr = (target === 0) ? 1 : ((stat - target) / target); 376 | let sqErr = percentErr * percentErr; 377 | scoreMSE = ((scoreMSE * countMSE) + sqErr * weight) / (countMSE + weight); 378 | countMSE++; 379 | } 380 | addToScore(stats.solutionDepth, this.params.targetSolutionDepth, 2); 381 | addToScore(stats.deadEnds, this.params.targetDeadEnds, 2); 382 | addToScore(stats.steps, this.params.targetTotalSteps, 1.5); 383 | // Minimize number of prefilled squares 384 | let numPrefilled = 0; 385 | for (let value of board.data) { 386 | if (value !== null) numPrefilled++; 387 | } 388 | addToScore(board.rows * board.cols - numPrefilled, board.rows * board.cols, 0.2); 389 | return scoreMSE; 390 | } 391 | 392 | /** 393 | * Builds a puzzle using the builder parameters. 394 | * 395 | * @method buildPuzzle 396 | * @return {Object} 397 | */ 398 | buildPuzzle() { 399 | let bestScore = null; 400 | let bestBoard = null; 401 | let bestStats = null; 402 | for (let i = 0; i < this.params.numPuzzleIterations; i++) { 403 | let lastScore = null; 404 | this._tryRandomPuzzle((board, stats) => { 405 | let score = this._scoreStats(stats, board); 406 | if (bestScore === null || score < bestScore) { 407 | bestScore = score; 408 | bestStats = stats; 409 | bestBoard = board; 410 | } 411 | if (lastScore !== null && lastScore < score) { 412 | return false; 413 | } 414 | lastScore = score; 415 | return true; 416 | }); 417 | } 418 | return { 419 | board: bestBoard, 420 | stats: bestStats, 421 | score: bestScore 422 | }; 423 | } 424 | } 425 | 426 | module.exports = Builder; 427 | 428 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const Board = require('./board'); 2 | const Solver = require('./solver'); 3 | const Builder = require('./builder'); 4 | 5 | let board = Board.makeRandomBoard(10, 10, 1); 6 | console.log('Randomly created board: (spaces are blank)'); 7 | board.printBoard(' ', '?'); 8 | console.log(''); 9 | 10 | let res = Builder.buildPuzzleFromData(board, 4); 11 | console.log('Created puzzle: (Xs are known blanks, spaces are unknowns)'); 12 | res.board.printBoard('X', ' '); 13 | console.log('Solution stats:'); 14 | console.log(res.stats); 15 | console.log(''); 16 | 17 | let solutions = Solver.findPossibleSolutions(res.board); 18 | console.log('Solution from solver:'); 19 | for (let sol of solutions) { 20 | sol.printBoard(' ', '?'); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Board: require('./board'), 3 | Solver: require('./solver'), 4 | Builder: require('./builder') 5 | }; 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nonogrammer", 3 | "version": "0.1.0", 4 | "description": "Nonogram solver and generator", 5 | "license": "BSD-2-Clause", 6 | "main": "index.js", 7 | "scripts": { 8 | "browserify": "./node_modules/.bin/browserify web/main.js -o web/bundle.js" 9 | }, 10 | "dependencies": { 11 | "md5": "^2.2.1", 12 | "objtools": "^1.6.5" 13 | }, 14 | "devDependencies": { 15 | "browserify": "^16.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /solver.js: -------------------------------------------------------------------------------- 1 | const objtools = require('objtools'); 2 | const deepCopy = objtools.deepCopy; 3 | const Board = require('./board'); 4 | 5 | /** 6 | * This class contains the logic for a nonogram solver. 7 | * 8 | * @class Solver 9 | * @constructor 10 | * @param {Board} board - The game board with unknowns the solver should solve. 11 | */ 12 | class Solver { 13 | 14 | constructor(board) { 15 | this.board = board; 16 | } 17 | 18 | /* 19 | * This is the main solver entry point. It is a static method that takes a Board object 20 | * with unknowns/nulls and returns an array of solved Board objects. 21 | * 22 | * This function primarily contains recursive logic to perform a depth first search on 23 | * puzzle board configurations. Additional solving logic to determine all cells that 24 | * can be determined within a given row/column is called "simple solving" here and is 25 | * implemented in other methods that are called from within this method's recursive logic. 26 | * 27 | * @method findPossibleSolutions 28 | * @param {Board} board 29 | * @return {Board[]} 30 | */ 31 | static findPossibleSolutions(board) { 32 | // Array of solution Boards found so far 33 | let solutions = []; 34 | // Number of board values 35 | const maxValue = board.getMaxValue(); 36 | // Contains a set of Board tokens that have been explored to avoid recomputing earlier paths. 37 | let visitedSet = {}; 38 | 39 | // Array of all possible cell values 40 | let allPossibleValues = []; 41 | for (let i = 0; i <= maxValue; i++) allPossibleValues.push(i); 42 | 43 | // Number of solver iterations so far 44 | let iterations = 0; 45 | 46 | // Recursive function that tries all board configurations accessible from the given solver 47 | function findSolutionsFromState(solver, depth = 0) { 48 | iterations++; 49 | // Try to simple-solve the board 50 | let simpleSolveResult = solver.simpleSolveBatch(); 51 | if (simpleSolveResult.contradiction) { 52 | // No solution to this puzzle 53 | return; 54 | } 55 | // Make sure we have not yet checked this board configuration 56 | let token = solver.board.makeToken(); 57 | if (visitedSet[token]) { 58 | return; 59 | } 60 | visitedSet[token] = true; 61 | 62 | // If there are no unknowns remaining, this is a valid solution 63 | if (simpleSolveResult.remainingUnknowns === 0) { 64 | solutions.push(Solver.partialCopyBoard(solver.board)); 65 | return; 66 | } 67 | 68 | // Find first unknown cell 69 | for (let row = 0; row < solver.board.rows; row++) { 70 | for (let col = 0; col < solver.board.cols; col++) { 71 | let value = solver.board.get(row, col); 72 | if (value === null || Array.isArray(value)) { 73 | // Set the cell value to each possible value and recurse, checking for solutions along each path 74 | let possibleValues = (value === null) ? allPossibleValues : value; 75 | for (let possibleValue of possibleValues) { 76 | solver.board.set(row, col, possibleValue); 77 | findSolutionsFromState(solver.partialDup(), depth + 1); 78 | solver.board.set(row, col, value); 79 | } 80 | return; 81 | } 82 | } 83 | } 84 | } 85 | 86 | findSolutionsFromState(new Solver(Solver.partialCopyBoard(board))); 87 | return solutions; 88 | } 89 | 90 | /** 91 | * Makes a copy of a Board that deep-copies board data and shallow-copies everything else. 92 | * 93 | * @method partialCopyBoard 94 | * @static 95 | * @param {Board} board 96 | * @return {Board} 97 | */ 98 | static partialCopyBoard(board) { 99 | // Deep-copies board data, shallow-copies everything else 100 | let b = new Board(board.rows, board.cols); 101 | b.rowClues = board.rowClues; 102 | b.colClues = board.colClues; 103 | b.data = deepCopy(board.data); 104 | return b; 105 | } 106 | 107 | /** 108 | * Duplicates this Solver with a partially copied Board. 109 | * 110 | * @method partialDup 111 | * @return {Solver} 112 | */ 113 | partialDup() { 114 | return new Solver(Solver.partialCopyBoard(this.board)); 115 | } 116 | 117 | /** 118 | * Repeatedly makes passes on the board and simple-solves all rows and columns 119 | * until no more can be simple-solved. 120 | * 121 | * The returned object contains: 122 | * - steps - Number of simple solve "steps" where each step involves inferring values for one row/col 123 | * - remainingUnknowns - Number of remaining unknown cells after simple solve batch 124 | * - contradiction - true if simple-solving lead to a logical contradiction, meaning the board is unsolveable 125 | * 126 | * @method simpleSolveBatch 127 | * @return {Object} 128 | */ 129 | simpleSolveBatch(simpleSolveCrossLines = true) { 130 | let maxValue = this.board.getMaxValue(); 131 | let numSteps = 0; 132 | for (;;) { 133 | let solvedOne = false; 134 | for (let row = 0; row < this.board.rows; row++) { 135 | let line = this.board.getRow(row); 136 | let res = Solver.simpleSolveLine(line, this.board.rowClues[row], maxValue, simpleSolveCrossLines); 137 | if (res === null) { 138 | return { 139 | steps: numSteps, 140 | contradiction: true 141 | }; 142 | } 143 | if (res[0] > 0) { 144 | numSteps++; 145 | solvedOne = true; 146 | this.board.setRow(row, line); 147 | } 148 | } 149 | for (let col = 0; col < this.board.cols; col++) { 150 | let line = this.board.getCol(col); 151 | let res = Solver.simpleSolveLine(line, this.board.colClues[col], maxValue, simpleSolveCrossLines); 152 | if (res === null) { 153 | return { 154 | steps: numSteps, 155 | contradiction: true 156 | }; 157 | } 158 | if (res[0] > 0) { 159 | numSteps++; 160 | solvedOne = true; 161 | this.board.setCol(col, line); 162 | } 163 | } 164 | if (!solvedOne) break; 165 | } 166 | let numUnknowns = 0; 167 | for (let value of this.board.data) { 168 | if (value === null) numUnknowns++; 169 | } 170 | return { 171 | steps: numSteps, 172 | remainingUnknowns: numUnknowns, 173 | contradiction: false 174 | }; 175 | } 176 | 177 | /** 178 | * Performs any simple solution steps possible given a single row or column. 179 | * 180 | * The return value is either `null` (indicating there are no valid line solutions), or 181 | * an array in the form [ numNewlySolvedCells, numRemainingUnknownCells ] 182 | * This function may transform some `null` elements into arrays of possible values. 183 | * 184 | * @method simpleSolveLine 185 | * @param {Number[]} line - The row or column value array. This is updated in-place with discovered values. 186 | * @param {Object[]} clues - Array of clue objects for the row/col, each containing { value: X, run: Y } 187 | * @param {Boolean} [simpleSolveCrossLines=true] - Whether to take into account partially know cells; increases difficulty 188 | * @return {Number[]|Null} 189 | */ 190 | static simpleSolveLine(line, clues, boardMaxValue, simpleSolveCrossLines = true) { 191 | // Make sure line contains at least one unknown 192 | let earlyCheckUnknowns = false; 193 | for (let value of line) { 194 | if (value === null || (Array.isArray(value) && value.length > 1)) { 195 | earlyCheckUnknowns = true; 196 | break; 197 | } 198 | } 199 | if (!earlyCheckUnknowns) return [ 0, 0 ]; 200 | 201 | if (!simpleSolveCrossLines) { 202 | // Remove any partially known cells 203 | for (let i = 0; i < line.length; i++) { 204 | if (Array.isArray(line[i]) && line[i].length > 1) { 205 | line[i] = null; 206 | } 207 | } 208 | } 209 | 210 | // Find all valid solutions for this line, and tabulate which cells are the same across all solutions for this line 211 | 212 | // Array of cell values matching the line length containing cell values that are the same across all line solutions 213 | let knownCells = null; 214 | let length = line.length; 215 | 216 | // Make a set of all the values in the line clues 217 | let lineValues = []; 218 | for (let clue of clues) { 219 | if (lineValues.indexOf(clue.value) < 0) lineValues.push(clue.value); 220 | } 221 | 222 | // Given a value, returns an array of possible values for that cell 223 | function toKnownArray(value, useWholeBoardMax = false) { 224 | if (Array.isArray(value)) return value; 225 | if (value === null) { 226 | if (useWholeBoardMax) { 227 | let r = []; 228 | for (let i = 0; i <= boardMaxValue; i++) r.push(i); 229 | return r; 230 | } else { 231 | let r = deepCopy(lineValues); 232 | r.push(0); 233 | return r; 234 | } 235 | } 236 | return [ value ]; 237 | } 238 | 239 | // Scalar array intersection 240 | function intersection(ar1, ar2) { 241 | let ret = []; 242 | for (let el of ar1) { 243 | if (ar2.indexOf(el) >= 0) { 244 | ret.push(el); 245 | } 246 | } 247 | return ret; 248 | } 249 | 250 | // Checks to see if 2 sets of values are the same 251 | function valueSetsEqual(a, b) { 252 | if (!Array.isArray(a) && !Array.isArray(b)) return a === b; 253 | if (!Array.isArray(a)) a = [ a ]; 254 | if (!Array.isArray(b)) b = [ b ]; 255 | if (a.length !== b.length) return false; 256 | for (let el of a) { 257 | if (b.indexOf(el) < 0) return false; 258 | } 259 | return true; 260 | } 261 | 262 | // Checks if the 'set' possible value set wholly contains the 'otherSet' 263 | function valueSetContains(set, otherSet) { 264 | if (set === null) return true; // unknown values can be anything 265 | set = toKnownArray(set); 266 | otherSet = toKnownArray(otherSet); 267 | for (let el of otherSet) { 268 | if (set.indexOf(el) < 0) return false; 269 | } 270 | return true; 271 | } 272 | 273 | // Remove el from set 274 | function valueSetRemove(set, el) { 275 | set = toKnownArray(set); 276 | let idx = set.indexOf(el); 277 | if (idx >= 0) { 278 | set.splice(idx, 1); 279 | } 280 | return set; 281 | } 282 | 283 | function union(a, b) { 284 | let res = []; 285 | a = toKnownArray(a); 286 | b = toKnownArray(b); 287 | for (let el of a) { 288 | if (res.indexOf(el) < 0) res.push(el); 289 | } 290 | for (let el of b) { 291 | if (res.indexOf(el) < 0) res.push(el); 292 | } 293 | return res; 294 | } 295 | 296 | // Given a valid line solution, compares it to `knownCells` and updates `knownCells` with all cells that are the same 297 | function trackPossibleLineSolution(curLine) { 298 | if (!knownCells) { 299 | knownCells = deepCopy(curLine); 300 | return; 301 | } 302 | for (let i = 0; i < curLine.length; i++) { 303 | let cur = curLine[i]; 304 | let known = knownCells[i]; 305 | if (cur === known) continue; 306 | knownCells[i] = union(toKnownArray(cur), toKnownArray(known)); 307 | } 308 | } 309 | 310 | // Recursive function that tries all possible positions of each clue 311 | // Parameters are: 312 | // - curLine - Array containing the line values 313 | // - clueIdx - The index of the clue to try positions on 314 | // - nextPossibleCluePos - The first index in curLine to start checking for this clue's position 315 | function tryCluePositions(curLine, clueIdx, nextPossibleCluePos) { 316 | // Degenerate case of 0 clues. Line must be all blank. 317 | if (clues.length === 0) { 318 | for (let i = 0; i < length; i++) curLine[i] = 0; 319 | trackPossibleLineSolution(curLine); 320 | return; 321 | } 322 | 323 | // Iterate through all possible clue positions in this line 324 | let clue = clues[clueIdx]; 325 | for (let pos = nextPossibleCluePos; pos < length; pos++) { 326 | if (valueSetsEqual(line[pos], 0)) { 327 | // We already know this is a space, so the run can't start here, but might start the next iteration. 328 | curLine[pos] = 0; 329 | continue; 330 | } 331 | if (!valueSetContains(line[pos], clue.value)) { 332 | if (valueSetContains(line[pos], 0)) { 333 | curLine[pos] = 0; 334 | continue; 335 | } 336 | // Clue position is of a value different from the clue. Run can't start here, or later on. 337 | break; 338 | } 339 | 340 | // Make sure the run won't overrun the end of the line 341 | if (pos + clue.run > length) { 342 | break; 343 | } 344 | 345 | // Make sure this run at this position is compatible with the rest of the line 346 | let curCheckPos; 347 | let foundDefiniteNonBlank = false; 348 | for (curCheckPos = pos; curCheckPos < pos + clue.run; curCheckPos++) { 349 | // Make sure this clue fits the known values of this spot 350 | if (!valueSetContains(line[curCheckPos], 0)) foundDefiniteNonBlank = true; 351 | if (!valueSetContains(line[curCheckPos], clue.value)) break; 352 | } 353 | // If we found an incompatibility ... 354 | if (curCheckPos !== pos + clue.run) { 355 | // Found a cell at curCheckPos that's incompatible with the clue 356 | // If any non-blanks have been found, either it's a different value, or an incompatible blank after finding 357 | // some of the clue's value and no further positions will be valid 358 | if (foundDefiniteNonBlank) { 359 | break; 360 | } 361 | // Otherwise, we hit a string of potential blanks, and the only possibly solution is if they are all blanks 362 | // Update curLine to prepare for skipping ahead 363 | for (let i = pos; i <= curCheckPos; i++) { 364 | curLine[i] = 0; 365 | } 366 | // Advance pos to that cell, so next loop through starts with checking the immediate next cell 367 | pos = curCheckPos; 368 | // Skip to next iteration 369 | continue; 370 | } 371 | 372 | // Next cell must be at end of line, or different value (including blank or unknown). 373 | if (pos + clue.run < length && valueSetsEqual(line[pos + clue.run], clue.value)) { 374 | // Can continue if starting pos can be blank 375 | if (valueSetContains(line[pos], 0)) { 376 | curLine[pos] = 0; 377 | continue; 378 | } else { 379 | break; 380 | } 381 | } 382 | 383 | // Remove this clue's value from the next cell's potential value set 384 | if (pos + clue.run < length) { 385 | // Remove this clue's value from the next cell's potential value set 386 | curLine[pos + clue.run] = valueSetRemove(curLine[pos + clue.run], clue.value); 387 | } 388 | 389 | // If this is the last clue, ensure all remaining cells are blank or unknown 390 | if (clueIdx === clues.length - 1) { 391 | let remainingCellsBlank = true; 392 | for (let i = pos + clue.run; i < length; i++) { 393 | if (!valueSetContains(line[i], 0)) { remainingCellsBlank = false; break; } 394 | curLine[i] = 0; 395 | } 396 | if (!remainingCellsBlank) { 397 | // We can advance if starting position is unknown (could be blank) 398 | if (valueSetContains(line[pos], 0)) { 399 | curLine[pos] = 0; 400 | continue; 401 | } else { 402 | // Advancing would leave un-accounted-for cell values 403 | break; 404 | } 405 | } 406 | } else { 407 | // If this is not the last clue: 408 | // Ensure we have room for more clues 409 | if (pos + clue.run >= length) { 410 | break; 411 | } 412 | // Ensure the next clue is a different value, or there is a space in between 413 | if (clues[clueIdx + 1].value === clue.value) { 414 | if (!valueSetContains(line[pos + clue.run], 0)) { 415 | // We can advance if starting position is unknown (could be blank) 416 | if (valueSetContains(line[pos], 0)) { 417 | curLine[pos] = 0; 418 | continue; 419 | } else { 420 | break; 421 | } 422 | } else { 423 | curLine[pos + clue.run] = 0; 424 | } 425 | } 426 | } 427 | 428 | // We now know that, so far, this is a valid configuration for this clue 429 | 430 | // Update curLine for these values 431 | for (let i = pos; i < pos + clue.run; i++) { 432 | curLine[i] = clue.value; 433 | } 434 | 435 | if (clueIdx === clues.length - 1) { 436 | // This is the last clue, and the recursive "base case" 437 | trackPossibleLineSolution(curLine); 438 | } else { 439 | // Calculate the next possible clue starting position 440 | let nextNPCP = pos + clue.run; 441 | if (clues[clueIdx + 1].value === clue.value) { 442 | // There must be a space in between. This is validated earlier but needs to be accounted for here. 443 | nextNPCP++; 444 | } 445 | // Recurse and try positions for the next clue 446 | tryCluePositions(curLine, clueIdx + 1, nextNPCP); 447 | } 448 | 449 | // We can continue if the starting cell is unknown 450 | if (valueSetContains(line[pos], 0)) { 451 | curLine[pos] = 0; 452 | continue; 453 | } else { 454 | break; 455 | } 456 | } 457 | } 458 | 459 | // Start trying clue positions, starting with the first clue at the beginning of the line 460 | tryCluePositions(deepCopy(line), 0, 0); 461 | 462 | // No valid line solutions were found, so it must not be solveable. 463 | if (!knownCells) return null; 464 | 465 | // Find the number of newly solved cells and remaining unknown cells 466 | // A "solved" cell here is any cell whose value set has been reduced 467 | // An unknown is any cell who has a value set length > 1 468 | let numSolved = 0; 469 | let numUnknowns = 0; 470 | for (let i = 0; i < knownCells.length; i++) { 471 | let knownPossibleValues = toKnownArray(knownCells[i]).length; 472 | let linePossibleValues = toKnownArray(line[i], true).length; 473 | let countsAsSolved = knownPossibleValues < linePossibleValues; 474 | if (!simpleSolveCrossLines) { 475 | countsAsSolved = knownPossibleValues === 1 && linePossibleValues > 1; 476 | } 477 | 478 | if (countsAsSolved) { 479 | numSolved++; 480 | } 481 | if (countsAsSolved || knownPossibleValues < linePossibleValues) { 482 | line[i] = knownCells[i]; 483 | } 484 | if (!valueSetContains(line[i], knownCells[i])) { 485 | // Should never happen 486 | throw new Error('Valid line solution contradicts line?'); 487 | } 488 | if (toKnownArray(line[i]).length !== 1) numUnknowns++; 489 | } 490 | 491 | // Simplify value sets of 1 element 492 | for (let i = 0; i < line.length; i++) { 493 | if (Array.isArray(line[i])) { 494 | if (line[i].length === 1) { 495 | line[i] = line[i][0]; 496 | } else if (line[i].length === boardMaxValue + 1) { 497 | line[i] = null; 498 | } 499 | } 500 | } 501 | 502 | return [ numSolved, numUnknowns ]; 503 | } 504 | 505 | } 506 | 507 | module.exports = Solver; 508 | 509 | 510 | -------------------------------------------------------------------------------- /tests/test1.js: -------------------------------------------------------------------------------- 1 | const { 2 | Solver, 3 | Builder, 4 | Board 5 | } = require('../index'); 6 | 7 | let board = new Board(11, 8); 8 | board.clearData(null); 9 | 10 | function mkc(clues) { 11 | return clues.map((c) => { return { value: 1, run: c }; }); 12 | } 13 | 14 | board.rowClues = [ 15 | [], 16 | mkc([ 4 ]), 17 | mkc([ 6 ]), 18 | mkc([ 2, 2 ]), 19 | mkc([ 2, 2 ]), 20 | mkc([ 6 ]), 21 | mkc([ 4 ]), 22 | mkc([ 2 ]), 23 | mkc([ 2 ]), 24 | mkc([ 2 ]), 25 | [] 26 | ]; 27 | 28 | board.colClues = [ 29 | [], 30 | mkc([ 9 ]), 31 | mkc([ 9 ]), 32 | mkc([ 2, 2 ]), 33 | mkc([ 2, 2 ]), 34 | mkc([ 4 ]), 35 | mkc([ 4 ]), 36 | [] 37 | ]; 38 | 39 | let sols = Solver.findPossibleSolutions(board); 40 | if (!sols) { 41 | console.log('null return, no solutions'); 42 | } else { 43 | for (let sol of sols) sol.printBoard(); 44 | } 45 | 46 | -------------------------------------------------------------------------------- /tests/test2.js: -------------------------------------------------------------------------------- 1 | const { 2 | Solver, 3 | Builder, 4 | Board 5 | } = require('../index'); 6 | 7 | let board = new Board(8, 8); 8 | board.data = [ 9 | 0, 0, 0, 0, 0, 0, 0, 0, 10 | 0, 1, 1, 1, 0, 0, 0, 0, 11 | 0, 0, 1, 1, 1, 0, 0, 1, 12 | 0, 0, 0, 0, 1, 0, 0, 0, 13 | 0, 1, 0, 1, 1, 0, 0, 0, 14 | 0, 0, 0, 0, 0, 0, 0, 0, 15 | 0, 0, 0, 0, 0, 1, 0, 0, 16 | 0, 1, 0, 0, 0, 0, 0, 0 17 | ]; 18 | 19 | 20 | let res = Builder.buildPuzzleFromData(board, 9); 21 | res.board.printBoard(); 22 | 23 | -------------------------------------------------------------------------------- /tests/test3.js: -------------------------------------------------------------------------------- 1 | const { 2 | Solver, 3 | Builder, 4 | Board 5 | } = require('../index'); 6 | 7 | let board = Board.makeRandomBoard(5, 5, 2); 8 | 9 | 10 | let res = Builder.buildPuzzleFromData(board, 4); 11 | res.board.printBoard(); 12 | 13 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |'); 142 | columnClueRow.append(topLeftSpacer); 143 | for (let colNum = 0; colNum < board.cols; colNum++) { 144 | let clues = board.colClues[colNum]; 145 | let colClueCell = $(' | ').addClass('nonogramColClueCell').data('col', colNum).data('nonoColClue', true);
146 | for (let clue of clues) {
147 | let clueDiv = $(' ').addClass('nonogramColClue').text('' + clue.run);
148 | if (palette && palette[clue.value] && palette[clue.value].color) {
149 | clueDiv.css('color', palette[clue.value].color);
150 | }
151 | colClueCell.append(clueDiv);
152 | }
153 | columnClueRow.append(colClueCell);
154 | }
155 | table.append(columnClueRow);
156 | // Build other rows
157 | for (let rowNum = 0; rowNum < board.rows; rowNum++) {
158 | let rowRow = $(' ').addClass('nonogramRowClueCell').data('row', rowNum).data('nonoRowClue', true);
160 | for (let clue of board.rowClues[rowNum]) {
161 | let rowClueSpan = $('').addClass('nonogramRowClue').text('' + clue.run);
162 | if (palette && palette[clue.value] && palette[clue.value].color) {
163 | rowClueSpan.css('color', palette[clue.value].color);
164 | }
165 | rowClueCell.append(rowClueSpan);
166 | }
167 | rowRow.append(rowClueCell);
168 | let rowData = board.getRow(rowNum);
169 | for (let colNum = 0; colNum < rowData.length; colNum++) {
170 | let value = rowData[colNum];
171 | let cell = $(' | ').addClass('nonogramDataCell').data('row', rowNum).data('col', colNum).data('nonoCell', true);
172 | setBoardCellValue(cell, value, palette);
173 | rowRow.append(cell);
174 | }
175 | table.append(rowRow);
176 | }
177 | return table;
178 | }
179 |
180 | let paletteColorSet = [ 'white', 'black', 'red', 'yellow', 'green', 'blue', 'orange', 'purple' ];
181 | let palette = [];
182 |
183 | function serializePalette(palette) {
184 | return palette.map((obj) => {
185 | return [
186 | obj.color || '',
187 | (obj.text && obj.textColor) || '',
188 | obj.text || ''
189 | ].join('.');
190 | }).join(',');
191 | }
192 |
193 | function deserializePalette(str) {
194 | let entries = str.split(',');
195 | let palette = [];
196 | for (let entryStr of entries) {
197 | let o = {};
198 | let parts = entryStr.split('.');
199 | if (parts[0]) o.color = parts[0];
200 | else o.color = 'white';
201 | if (parts[1]) o.textColor = parts[1];
202 | if (parts[2]) o.text = parts[2];
203 | for (let idx = 0; idx < paletteColorSet.length; idx++) {
204 | if (o.color === paletteColorSet[idx]) {
205 | o.colorIdx = idx;
206 | break;
207 | }
208 | }
209 | if (o.colorIdx === undefined) {
210 | o.colorIdx = paletteColorSet.length;
211 | paletteColorSet.push(o.color);
212 | }
213 | palette.push(o);
214 | }
215 | return palette;
216 | }
217 |
218 | function paletteSelectorAddColor(onChange = null) {
219 | if (palette.length >= paletteColorSet.length) return;
220 | let idx = palette.length;
221 | palette.push({ color: paletteColorSet[idx], colorIdx: idx });
222 | let colorSpan = $('').addClass('nonogramPalSelBlock');
223 | colorSpan.css('background-color', palette[idx].color);
224 | //$('#paletteSelector').append(colorSpan);
225 | //palette[idx].el = colorSpan;
226 | colorSpan.click(function() {
227 | if (idx === 0) return;
228 | palette[idx].colorIdx++;
229 | if (palette[idx].colorIdx >= paletteColorSet.length) palette[idx].colorIdx = 0;
230 | palette[idx].color = paletteColorSet[palette[idx].colorIdx];
231 | colorSpan.css('background-color', palette[idx].color);
232 | if (onChange) onChange();
233 | });
234 | let colorSpanPadding = $('').addClass('nonogramPalSelBlockPad');
235 | colorSpanPadding.append(colorSpan);
236 | $('#paletteSelector').append(colorSpanPadding);
237 | palette[idx].el = colorSpanPadding;
238 | }
239 |
240 | function paletteSelectorRemoveColor() {
241 | if (palette.length < 3) return;
242 | palette.pop().el.remove();
243 | }
244 |
245 | function resetPaletteSelector(onChange = null) {
246 | palette = [];
247 | $('#paletteSelector').empty();
248 | paletteSelectorAddColor(onChange);
249 | paletteSelectorAddColor(onChange);
250 | palette.unknown = { color: 'white' };
251 | }
252 |
253 | function initPaletteSelector(callbacks = {}) {
254 | resetPaletteSelector(callbacks.onChange);
255 | $('#paletteAddButton').off('click').click(() => {
256 | paletteSelectorAddColor(callbacks.onChange);
257 | if (callbacks.onAdd) callbacks.onAdd();
258 | });
259 | $('#paletteRemoveButton').off('click').click(() => {
260 | paletteSelectorRemoveColor();
261 | if (callbacks.onRemove) callbacks.onRemove();
262 | });
263 | }
264 |
265 | function initResizeSelector(board, boardEl, defaultValue = 0, resizeCb = null) {
266 | $('#addRowButton').off('click').click(() => {
267 | board.resize(board.rows + 1, board.cols, defaultValue);
268 | refreshPuzzleUI(board, boardEl, palette);
269 | if (resizeCb) resizeCb();
270 | });
271 | $('#removeRowButton').off('click').click(() => {
272 | board.resize(board.rows - 1, board.cols, defaultValue);
273 | refreshPuzzleUI(board, boardEl, palette);
274 | if (resizeCb) resizeCb();
275 | });
276 | $('#addColButton').off('click').click(() => {
277 | board.resize(board.rows, board.cols + 1, defaultValue);
278 | refreshPuzzleUI(board, boardEl, palette);
279 | if (resizeCb) resizeCb();
280 | });
281 | $('#removeColButton').off('click').click(() => {
282 | board.resize(board.rows, board.cols - 1, defaultValue);
283 | refreshPuzzleUI(board, boardEl, palette);
284 | if (resizeCb) resizeCb();
285 | });
286 | }
287 |
288 | let mouseX = 0, mouseY = 0;
289 |
290 | function initTrackMouse() {
291 | $('body').mousemove(function(event) {
292 | mouseX = event.pageX;
293 | mouseY = event.pageY;
294 | });
295 | }
296 |
297 | let showingEditCluePopUp = false;
298 |
299 | function editCluePopUp(lineClues, cb) {
300 | if (showingEditCluePopUp) return false;
301 | let inputEl = $('').attr('type', 'text').attr('size', '20');
302 | inputEl.css('position', 'absolute');
303 | inputEl.css('top', '' + mouseY + 'px');
304 | inputEl.css('left', '' + mouseX + 'px');
305 |
306 | let initStr = lineClues.map((clue) => {
307 | if (clue.value !== 1) {
308 | return `${clue.run}/${clue.value}`;
309 | } else {
310 | return '' + clue.run;
311 | }
312 | }).join(' ');
313 | inputEl.val(initStr);
314 |
315 | $('body').append(inputEl);
316 | inputEl.focus();
317 | inputEl.on('keyup', function(event) {
318 | if (event.keyCode === 13) {
319 | let str = inputEl.val();
320 | inputEl.remove();
321 | showingEditCluePopUp = false;
322 | let parts = str.split(/[^0-9\/]+/);
323 | let clues = [];
324 | for (let part of parts) {
325 | if (part) {
326 | let partParts = part.split('/');
327 | let value = parseInt(partParts[1] || 1);
328 | let run = parseInt(partParts[0] || 1);
329 | clues.push({ value, run });
330 | }
331 | }
332 | cb(clues);
333 | }
334 | });
335 | showingEditCluePopUp = true;
336 | return true;
337 | }
338 |
339 | function initEditBoard(board, boardEl, allowUnknown, allowEditClues, onChange, onClueChange = null) {
340 | boardEl.find('.nonogramDataCell').off('mousedown').mousedown((event) => {
341 | let el = $(event.target);
342 | let row = parseInt(el.data('row'));
343 | let col = parseInt(el.data('col'));
344 | let value = board.get(row, col);
345 | let newValue = value;
346 | if (event.which === 1) {
347 | // left click cycles between colors
348 | if (value === null) {
349 | newValue = 1;
350 | } else if (value === 0 && allowUnknown) {
351 | newValue = null;
352 | } else {
353 | newValue = value + 1;
354 | if (newValue >= palette.length) {
355 | newValue = 0;
356 | }
357 | }
358 | } else if (event.which === 3) {
359 | // right click toggles between unknown and blank
360 | if (!allowUnknown) return;
361 | if (value === null) newValue = 0;
362 | else newValue = null;
363 | }
364 | if (newValue !== value) {
365 | board.set(row, col, newValue);
366 | let res = onChange(row, col, newValue, value);
367 | if (res === false) {
368 | board.set(row, col, value);
369 | }
370 | setBoardCellValue(el, board.get(row, col), palette);
371 | }
372 | }).on('contextmenu', () => false);
373 |
374 | boardEl.find('.nonogramRowClueCell, .nonogramColClueCell').off('mousedown');
375 | if (allowEditClues) {
376 |
377 | boardEl.find('.nonogramRowClueCell').click(function() {
378 | let rowNum = parseInt($(this).data('row'));
379 | editCluePopUp(board.rowClues[rowNum] || [], (clues) => {
380 | board.rowClues[rowNum] = clues;
381 | refreshPuzzleUI(board, boardEl, palette);
382 | });
383 | });
384 |
385 | boardEl.find('.nonogramColClueCell').click(function() {
386 | let colNum = parseInt($(this).data('col'));
387 | editCluePopUp(board.colClues[colNum] || [], (clues) => {
388 | board.colClues[colNum] = clues;
389 | refreshPuzzleUI(board, boardEl, palette);
390 | });
391 | });
392 | }
393 | }
394 |
395 | function disableEditBoard(boardEl) {
396 | boardEl.find('.nonogramDataCell, .nonogramRowClueCell, .nonogramColClueCell').off('mousedown');
397 | }
398 |
399 | function boardToKey(board) {
400 | let keyHex = md5(board.data.join(','));
401 | let keyBytes = aesjs.utils.hex.toBytes(keyHex);
402 | return keyBytes;
403 | }
404 |
405 | function makePlayLink(board, palette = null, solutionBoard = null) {
406 | let url = ('' + window.location).split('?')[0];
407 | url += '?mode=play&puzzle=' + board.serialize();
408 | if (palette) {
409 | url += '&palette=' + serializePalette(palette);
410 | }
411 | let message = $('#generateMessage').val();
412 | if (message && solutionBoard) {
413 | let messageBytes = aesjs.utils.utf8.toBytes(message);
414 | let keyBytes = boardToKey(solutionBoard);
415 | let aesCtr = new aesjs.ModeOfOperation.ctr(keyBytes);
416 | let encBytes = aesCtr.encrypt(messageBytes);
417 | let encHex = aesjs.utils.hex.fromBytes(encBytes);
418 | url += '&msg=' + encHex;
419 | }
420 | return url;
421 | }
422 |
423 | function getSolvedMessage(board) {
424 | let msgParam = getURLParam('msg');
425 | if (!msgParam) return 'Solved!';
426 | let encBytes = aesjs.utils.hex.toBytes(msgParam);
427 | let keyBytes = boardToKey(board);
428 | let aesCtr = new aesjs.ModeOfOperation.ctr(keyBytes);
429 | let message = aesCtr.decrypt(encBytes);
430 | return aesjs.utils.utf8.fromBytes(message);
431 | }
432 |
433 | function initBuilder(allowUnknown, allowEditClues, editCb, clueEditCb) {
434 | $('#paletteSelectorContainer').show();
435 | $('#resizeContainer').show();
436 | $('#puzzleContainer').empty();
437 | $('#solvedMessage').hide();
438 |
439 | let width = getURLParamInt('w', 5);
440 | let height = getURLParamInt('h', 5);
441 |
442 | let board = new nonogrammer.Board(height, width);
443 | board.clearData(allowUnknown ? null : 0);
444 |
445 | let boardEl = makePuzzleUI(board, palette);
446 | initEditBoard(board, boardEl, allowUnknown, allowEditClues, editCb, clueEditCb);
447 |
448 | initPaletteSelector({
449 | onRemove() {
450 | for (let i = 0; i < board.data.length; i++) {
451 | if (board.data[i] !== null && board.data[i] >= palette.length) {
452 | board.data[i] = 0;
453 | }
454 | }
455 | board.buildCluesFromData();
456 | refreshPuzzleUI(board, boardEl, palette);
457 | updatePageLayout();
458 | },
459 | onChange() {
460 | refreshPuzzleUI(board, boardEl, palette);
461 | updatePageLayout();
462 | }
463 | });
464 | if (allowUnknown) {
465 | palette[0] = { color: 'white', textColor: 'grey', text: 'X' };
466 | palette.unknown = { color: 'white' };
467 | } else {
468 | palette[0] = { color: 'white' };
469 | }
470 |
471 | initResizeSelector(board, boardEl, allowUnknown ? null : 0, () => {
472 | initEditBoard(board, boardEl, allowUnknown, allowEditClues, editCb, clueEditCb);
473 | updatePageLayout();
474 | });
475 |
476 | refreshPuzzleUI(board, boardEl, palette);
477 |
478 | $('#puzzleContainer').append(boardEl);
479 |
480 | updatePageLayout();
481 |
482 | return {
483 | board,
484 | boardEl
485 | };
486 | }
487 |
488 | function initSolveMode() {
489 | $('.pageTitle').text('Nonogram Puzzle Solver');
490 | showBlurb('solve');
491 |
492 | let builder;
493 | builder = initBuilder(true, true, (row, col) => {
494 | refreshPuzzleUI(builder.board, builder.boardEl, palette);
495 | updatePageLayout();
496 | }, () => updatePageLayout());
497 | $('#solveContainer').show();
498 |
499 | $('#solveButton').off('click').click(() => {
500 | let solutions;
501 | try {
502 | solutions = nonogrammer.Solver.findPossibleSolutions(nonogrammer.Solver.partialCopyBoard(builder.board));
503 | } catch (ex) {
504 | solutions = [];
505 | }
506 | $('#solutionsContainer').empty();
507 | $('#solutionsHeader').show();
508 | if (!solutions.length) {
509 | $('#solutionsContainer').text('No solution.');
510 | } else {
511 | let solutionPalette = objtools.deepCopy(palette);
512 | delete solutionPalette[0].text;
513 | for (let solution of solutions) {
514 | let solutionBoardUI = makePuzzleUI(solution, solutionPalette);
515 | let solutionDiv = $(' | ').addClass('solutionDiv');
516 | solutionDiv.append(solutionBoardUI);
517 | $('#solutionsContainer').append(solutionDiv);
518 | }
519 | }
520 | updatePageLayout();
521 | });
522 |
523 | updatePageLayout();
524 | }
525 |
526 | function initBuildMode() {
527 | $('.pageTitle').text('Nonogram Puzzle Builder');
528 | showBlurb('build');
529 |
530 | let builder;
531 | builder = initBuilder(false, false, (row, col) => {
532 | builder.board.buildCluesFromData();
533 | refreshPuzzleUI(builder.board, builder.boardEl, palette);
534 | updatePageLayout();
535 | });
536 | $('#generateContainer').show();
537 | $('#generateButton').off('click').click(() => {
538 | let difficulty = parseInt($('#generateDifficulty').val());
539 | if (typeof difficulty !== 'number') difficulty = 3;
540 | if (difficulty < 1) difficulty = 1;
541 | if (difficulty > 10) difficulty = 10;
542 | let buildResult = nonogrammer.Builder.buildPuzzleFromData(builder.board, difficulty);
543 | let resultPalette = objtools.deepCopy(palette);
544 | resultPalette.unknown = { color: 'white' };
545 | resultPalette[0] = { color: 'white', textColor: 'grey', text: 'X' };
546 |
547 | $('#generateLinkContainer').show();
548 | $('#generateLink').attr('href', makePlayLink(buildResult.board, resultPalette, builder.board));
549 | let buildResultEl = makePuzzleUI(buildResult.board, resultPalette);
550 | $('#generatePuzzleContainer').empty().append(buildResultEl);
551 | console.log('Built puzzle stats', buildResult.stats);
552 | updatePageLayout();
553 | });
554 | updatePageLayout();
555 | }
556 |
557 | function initPlayMode() {
558 | $('.pageTitle').text('Nonogram Puzzle');
559 | showBlurb('play');
560 |
561 | $('#paletteSelectorContainer').hide();
562 | $('#puzzleContainer').empty();
563 | $('#solvedMessage').hide();
564 | $('#resizeContainer').hide();
565 | $('#generateContainer').hide();
566 | $('#solveContainer').hide();
567 |
568 | let width;
569 | let height;
570 | let colors;
571 | let difficulty;
572 |
573 | let puzzleParamStr = getURLParam('puzzle');
574 | let puzzleParamBoard;
575 |
576 | if (puzzleParamStr) {
577 | puzzleParamBoard = nonogrammer.Board.deserialize(puzzleParamStr);
578 | width = puzzleParamBoard.cols;
579 | height = puzzleParamBoard.rows;
580 | colors = puzzleParamBoard.getMaxValue();
581 | difficulty = 3;
582 | } else {
583 | width = getURLParamInt('w', 5);
584 | height = getURLParamInt('h', 5);
585 | colors = getURLParamInt('colors', 1);
586 | difficulty = getURLParamInt('difficulty', 3);
587 | }
588 |
589 | $('#playNextWidth').val('' + width);
590 | $('#playNextHeight').val('' + height);
591 | $('#playNextColors').val('' + colors);
592 | $('#playNextDifficulty').val('' + difficulty);
593 |
594 | let emptyBoard, buildResults;
595 | if (puzzleParamBoard) {
596 | emptyBoard = puzzleParamBoard;
597 | } else {
598 | let filledBoard = nonogrammer.Board.makeRandomBoard(height, width, colors);
599 | buildResults = nonogrammer.Builder.buildPuzzleFromData(filledBoard, difficulty);
600 | emptyBoard = buildResults.board;
601 | }
602 |
603 | let puzzleBoard = nonogrammer.Solver.partialCopyBoard(emptyBoard);
604 | if (buildResults) {
605 | console.log('Created board solution stats: ', buildResults.stats);
606 | }
607 | resetPaletteSelector();
608 | palette[0] = { color: 'white', textColor: 'grey', text: 'X' };
609 |
610 | let paletteParamStr = getURLParam('palette');
611 | if (paletteParamStr) {
612 | palette = deserializePalette(paletteParamStr);
613 | }
614 | palette.unknown = { color: 'white' };
615 |
616 | let maxValue = emptyBoard.getMaxValue();
617 | while (palette.length <= maxValue) paletteSelectorAddColor();
618 | let boardEl = makePuzzleUI(puzzleBoard, palette);
619 |
620 | function checkValid() {
621 | if (puzzleBoard.validate(true)) {
622 | // Solved the puzzle
623 | // Transform all unknowns to blanks, and update the palette
624 | palette[0] = { color: 'white', text: '' };
625 | for (let row = 0; row < puzzleBoard.rows; row++) {
626 | for (let col = 0; col < puzzleBoard.cols; col++) {
627 | let value = puzzleBoard.get(row, col);
628 | if (value === null) {
629 | puzzleBoard.set(row, col, 0);
630 | }
631 | }
632 | }
633 | refreshPuzzleUI(puzzleBoard, boardEl, palette);
634 | disableEditBoard(boardEl);
635 | $('#solvedMessageText').text(getSolvedMessage(puzzleBoard));
636 | $('#solvedMessage').show();
637 | }
638 | }
639 |
640 | initEditBoard(puzzleBoard, boardEl, true, false, (row, col) => {
641 | if (emptyBoard.get(row, col) !== null) return false;
642 | checkValid();
643 | });
644 | $('#puzzleContainer').append(boardEl);
645 | updatePageLayout();
646 |
647 | window.solveNonogram = function() {
648 | let solutions = nonogrammer.Solver.findPossibleSolutions(puzzleBoard);
649 | if (!solutions.length) {
650 | alert('No solution');
651 | return;
652 | }
653 | for (let i = 0; i < puzzleBoard.data.length; i++) {
654 | puzzleBoard.data[i] = solutions[0].data[i];
655 | }
656 | refreshPuzzleUI(puzzleBoard, boardEl, palette);
657 | checkValid();
658 | };
659 | }
660 |
661 | function showBlurb(mode) {
662 | $('.pageBlurb').hide();
663 | if (mode === 'play') $('#pageBlurbPlay').show();
664 | else if (mode === 'build') $('#pageBlurbBuild').show();
665 | else if (mode === 'solve') $('#pageBlurbSolve').show();
666 | }
667 |
668 | $(function() {
669 |
670 | //let board = nonogrammer.Board.makeRandomBoard(10, 10, 1);
671 | //$('body').append(makePuzzleUI(board));
672 | if (mode === 'play') {
673 | initPlayMode();
674 | } else if (mode === 'build') {
675 | initBuildMode();
676 | } else if (mode === 'solve') {
677 | initSolveMode();
678 | }
679 |
680 | initTrackMouse();
681 |
682 | });
683 |
684 | })();
685 |
686 |
--------------------------------------------------------------------------------
/web/nonogram.css:
--------------------------------------------------------------------------------
1 | #pageContainer {
2 | margin: auto;
3 | min-width: 300px;
4 | }
5 |
6 | .pageTitle {
7 | text-align: center;
8 | }
9 |
10 | .nonogramDataCell {
11 | border: 1px solid grey;
12 | width: 30px;
13 | height: 30px;
14 | text-align: center;
15 | vertical-align: center;
16 | font-size: 15px;
17 | }
18 |
19 | .nonogramTable {
20 | border-collapse: collapse;
21 | border: 2px solid grey;
22 | margin: auto;
23 | }
24 |
25 | .nonogramColClueCell {
26 | border: 2px solid grey;
27 | text-align: center;
28 | vertical-align: bottom;
29 | height: 20px;
30 | }
31 |
32 | .nonogramColClue {
33 | font-size: 20px;
34 | color: black;
35 | }
36 |
37 | .nonogramRowClueCell {
38 | border: 2px solid grey;
39 | text-align: right;
40 | vertical-align: center;
41 | width: 20;
42 | }
43 |
44 | .nonogramRowClue {
45 | font-size: 20px;
46 | color: black;
47 | margin: 7px;
48 | }
49 |
50 | .nonogramPalSelBlock {
51 | width: 60px;
52 | height: 60px;
53 | border-style: solid;
54 | border-width: 2px;
55 | border-color: grey;
56 | display: inline-block;
57 | }
58 |
59 | #paletteAddRemoveSpan {
60 | display: inline-block;
61 | width: 40;
62 | }
63 |
64 | .nonogramPalSelBlockPad {
65 | margin: 3px;
66 | }
67 |
68 | .nonogramPalSelButtonSpan {
69 | float: left;
70 | display: block;
71 | }
72 |
73 | .nonogramPalButton {
74 | width: 25;
75 | height: 25;
76 | margin: 3px;
77 | }
78 |
79 | #paletteSelectorContainer {
80 | display: none;
81 | margin-bottom: 20px;
82 | }
83 |
84 | #resizeContainer {
85 | display: none;
86 | margin: 20px;
87 | }
88 |
89 | .resizeButtonDiv {
90 | margin: 5px;
91 | }
92 |
93 | #generateContainer {
94 | display: none;
95 | margin: 10px;
96 | }
97 |
98 | #generatePuzzleContainer {
99 | margin: 10px;
100 | }
101 |
102 | #generateLinkContainer {
103 | display: none;
104 | }
105 |
106 | #solveContainer {
107 | display: none;
108 | margin: 10px;
109 | }
110 |
111 | #solutionsHeader {
112 | display: none;
113 | margin: 15px;
114 | }
115 |
116 | .solutionDiv {
117 | margin: 10px;
118 | }
119 |
120 | .pageBlurbContainer {
121 | margin: 20px;
122 | }
123 |
124 | .pageBlurb {
125 | display: none;
126 | width: 400px;
127 | }
128 |
129 |
--------------------------------------------------------------------------------
/web/nonogram.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
140 |
141 |
--------------------------------------------------------------------------------
14 |
58 |
15 |
31 | 16 | This is a nonogram puzzle. The 17 | goal of the puzzle is to fill in the cells of the grid to match the clues for each row 18 | and column. Each clue represents a sequence of adjacent cells of the same color. 19 | Each sequence of cells must be separated by at least 1 blank space (or, for multi-color 20 | puzzles, may either be separated by a blank space or must be different colored sequences). 21 | More detailed rules are explained at 22 | the Wikipedia article. 23 | 24 |25 | To solve the puzzle, click on cells to toggle between filled and blank (or colors, for 26 | multi-colored puzzles). Cells can also be marked with an 'X', to indicate that the cell 27 | is definitely blank, by right-clicking. The puzzle will automatically detect when solved 28 | and may display a secret message if one was included with the puzzle. 29 | 30 |
32 |
49 | 33 | This is a nonogram builder. 34 | It allows you to create a solution image, and the builder will generate a puzzle that will 35 | result in that image. 36 | Click 37 | on cells in the grid to toggle between colors. Once you have created your desired configuration, 38 | select the desired puzzle difficulty, and click 'Generate Puzzle'. This will display the 39 | generated puzzle, guaranteed to have a single solution and be solvable within the selected 40 | difficulty level. It will also generate a link that can be given to others to solve. This 41 | link can optionally include a hidden message, encrypted such that it can only be decoded when 42 | the puzzle is solved. 43 | 44 |45 | Multi-color puzzles can be created by adding colors to the palette. Clicking on colors in 46 | the palette changes the color. 47 | 48 |
50 |
57 | 51 | This is a nonogram solver. Click on 52 | the empty clue spaces to specify clues. A text box pops up. Clues should be listed 53 | space-separated. Colored clues can be specified with "clue/colornumber". Colors are numbered 54 | starting from 1 (normally black). Cells can also be prefilled as definitely known or definitely a color. 55 | 56 |
59 |
77 | Color Palette60 |
61 |
62 |
63 |
64 |
65 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
78 |
79 |
104 |
105 |
114 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
115 |
131 |
116 | Difficulty (1-10):
117 |
118 |
119 |
120 |
121 | Solved message:
122 |
123 |
124 |
125 | Link to play:
126 | Play!
127 |
128 |
129 |
130 |
132 |
139 |
133 |
134 |
135 | Possible solutions:136 |
137 |
138 | |