angular.module('kenkenApp') .service('KenkenSolver', function() { // TODO go back to coords with cages? this.getSolver = function($scope) { // // MARK: solver variables // var board = $scope.board; // the grid var boardSize = board.length; // size of grid var rows = board; // the grid rows var columns = rowsToColumns(board); // the grid columns var rowsAndColumns = rows.concat(columns); // both var rowTotal = arithSum(boardSize); // sum of cells in each row var rowProduct = factorial(boardSize); // product of cells in each row var cages = []; // math cages in the board, plus new ones we'll make var cageExists = {}; // check this to avoid duplicates when we make new cages initializeCages(); // the rules used by the solver, in order var ruleNames = ["one or none", "singleton", "multiplication", "addition", "subtraction", "division", "must-have divisor", "pigeonhole", "two pair", "three", "two and two", "not both", "must have in line", "line sum", "line product", "double math", "subtract subcage"]; //ruleNames = ["not both"]; var rule; var numPasses; // iterator for stepwise solving var stepIterator = null; var done = false; var broken = null; var message; var logging = false; // the object that will be returned var solver = { solve: solveFully, step: step, reset: reset, message: function() { return message; }, rule: function() { return rule }, pass: function() { return numPasses; }, isActive: function() { return isActive; } }; // // MARK: main solving routine // // TODO maintain isActive // TODO display wrong on UI // the main routine function *solve() { if (done || broken) return; var maxPasses = 50; for (numPasses = 1; !done && numPasses < maxPasses; numPasses++) { var previousBoard = angular.copy(board); for (var ruleIndex = 0; !done && ruleIndex < ruleNames.length; ruleIndex++) { rule = ruleNames[ruleIndex]; console.log("RULE:", rule); var iterator = rules[rule](); for (var step = iterator.next(); !done && !step.done; step = iterator.next()) { yield null; } } if (!done && possiblesMatch(previousBoard)) yield *beDone(); } /* TODO make a judgment whether it's worth it to look for solutions (limit # of unsolved cells for example) if (!$scope.solved) { var numSolutions = countSolutions(board, $scope.cages, 0); console.log(numSolutions + " solutions!"); } */ yield null; rule = null; message = null; done = false; broken = null; stepIterator = null; } function step() { if (!stepIterator) stepIterator = solve(); return stepIterator.next(); } function solveFully() { while (!step().done) {} } // // MARK: initialization and resetting // function initializeCages() { // reset cages cages = []; cageExists = {}; // copy puzzle cages to our solver's cage list, with real cells inside instead of coordinates $scope.cages.forEach(function(c) { var cage = angular.copy(c); cage.cells.forEach(function(coords, i) { cage.cells[i] = board[coords[0]][coords[1]]; }); addCage(cage); }); // add cages for row/column total and row/column product rowsAndColumns.forEach(function(cells) { addCage({op: "+", total: rowTotal, cells: cells, collisionChecked: true}); addCage({op: "x", total: rowProduct, cells: cells, collisionChecked: true}); }); } function reset() { // in each cell, reset solution, guess, and possible values rows.forEach(function (cells) { cells.forEach(function (cell) { cell.possible = pNew(boardSize); delete cell.guess; }) }); initializeCages(); stepIterator = null; done = false; broken = null; } // // MARK: managing possible values in cells // // check possible values in previous board against current board function possiblesMatch(previousBoard) { for (var i = 0; i < boardSize; i++) { for (var j = 0; j < boardSize; j++) { if (!board[i][j].possible.matches(previousBoard[i][j].possible)) return false; } } return true; } // eliminate possible values in a cell function *clear(cell, values, why, ui) { if (!(values instanceof Array)) values = [values]; if (pYes(cell.possible, values)) { values = values.filter(function(v) { return (pYes(cell.possible, v)); }); setUI({ cell: cell, action: "clear " + values.join(", "), why: why }.extend(ui)); yield null; pClear(cell.possible, values); yield *checkOneOrNone(cell); } } // set a single value as the only possibility in a cell function *setOnly(cell, n, why, ui) { if (cell.guess != n) { setUI({ cell: cell, action: "set " + n, why: why }.extend(ui)); yield null; pSetOnly(cell.possible, n); yield *solveCell(cell); } } // set a cell to a particular value, and clear that value from other cells in its row and column // if the cell lives in a cage of 3 or more cells, make a smaller cage with the remaining cells function *solveCell(cell) { var n = pFirstValue(cell.possible); if (cell.ans != n) console.log("!!!!! WRONG"); // TODO better UI for this // set solution $scope.guess(cell.i, cell.j, n); if ($scope.solved) { yield *beDone(); return } // clear row and column var rowAndColumn = rows[cell.i].concat(columns[cell.j]); rowAndColumn.forEach(function(c) { if (!c.guess) pClear(c.possible, n); }); setUI({ cell: cell, highlight: rowAndColumn, action: "CELL SOLVED", why: "clear " + n + " from row and column" }); yield null; // check row & column for new solves or broken yield *rowAndColumn.yieldEach(checkOneOrNone); // find unsolved cells in cage var cage = cages[cell.cage]; yield *addReducedCage(cage); } // if cell has only one possible value, solve it; if it has none, be broken function *checkOneOrNone(cell) { if (!cell.guess) { switch (pCount(cell.possible)) { case 0: yield *beBroken(cell); break; case 1: yield *solveCell(cell); break; } } } // remove all values in the cage that make the cage math impossible to satisfy function *removeCageKillers(cage) { yield *cage.cells.yieldEach(function*(cell) { if (cage.cells.length > 1) { var invalid = cageKillers(cage, cell); if (invalid.length > 0) yield *clear(cell, invalid, "can't satisfy " + math(cage) + " with other cells", {highlight: cage.cells}); } }); } // identify values in a given cell that would make the cage math impossible to satisfy function cageKillers(cage, cell) { var killers = []; var otherCells = arraySubtract(cage.cells, [cell]); var op = cage.op, total = cage.total; var p = rowAndColumnPossibles(); pEach(cell.possible, function (n) { clearLines(cell, n); var subtotal = opReduce(op, total, n); if (!(otherCells.length == 1 ? pYes(otherCells[0].possible, subtotal) : canFinish(op, subtotal, otherCells))) { killers.push(n); } restoreLines(cell, n); }); function clearLines(cell, n) { pClear(p[cell.i], n); pClear(p[cell.j + boardSize], n); } function restoreLines(cell, n) { pSet(p[cell.i], n); pSet(p[cell.j + boardSize], n); } function canFinish(op, total, cells) { var cell = cells[0], restOfCells = cells.slice(1); var row = cell.i, column = cell.j + boardSize; function possible(n) { return pYes(cell.possible, n) && pYes(p[row], n) && pYes(p[column], n); } if (cells.length == 1) return possible(total); for (var n = 1; n <= boardSize; n++) { if (possible(n) && opLegal(op, total, n, boardSize)) { clearLines(cell, n); if (canFinish(op, opReduce(op, total, n), restOfCells)) { restoreLines(cell, n); return true; } else { restoreLines(cell, n); } } } return false; } return killers; } // // MARK: UI, broken, done // function setUI(args) { if (args.cell) $scope.setCursor(args.cell.i, args.cell.j); $scope.cursorHidden = args.cell ? false : true; $scope.cursorShadow = args.highlight; $scope.lit = args.lit; $scope.softLit = args.softLit; message = args.action + "
" + args.why; var log = (args.cell ? cellName(args.cell) + " " : "") + args.action + ": " + args.why; if (logging) console.log(log); } function *beBroken(cell) { setUI({ cell: cell, action: "BROKEN!", why: "cell has no possibles left" }); broken = true; done = true; yield null; } function *beDone() { setUI({ action: "DONE!", why: $scope.solved ? "puzzle solved" : "no further solving possible" }); done = true; yield null; } // // MARK: managing cages // function solveCage(cage) { cage.solved = true; console.log("CAGE SOLVED", cageName(cage)); } // TODO use setUI // add a cage to the cage list function addCage(cage, why) { var key = cage.op; cage.cells.forEach(function (cell) { key += ";" + cell.i + "," + cell.j; }); if (cageExists[key]) return false; if (why) setUI({ action: "add cage " + math(cage), why: why, highlight: cage.cells }); if (cage.cells[0].cage == cage.id) { // the puzzle's original cages are guaranteed not to overlap cage.collisionChecked = true; cage.subcageChecked = true; } cages.push(cage); cageExists[key] = true; cage.line = oneLine(cage.cells); return true; } function *addCageAndYield(cage, why, ui) { if (!"+x".includes(cage.op)) return; // only + and x cages can be added by solver var reduced = reduceCage(cage); switch (reduced.cells.length) { case 0: // no unsolved cells? don't add break; case 1: // one cell? solve it, don't add console.log("here!") yield *setOnly(reduced.cells[0], reduced.total, why, {highlight: cage.cells}.extend(ui)); break; default: // same number of cells as possible values? then cage math tells us nothing; don't add if (pCount(allPossible(reduced.cells)) == reduced.cells.length) break; if (addCage(reduced, why)) yield null; } } function *addReducedCage(cage) { var reduced = reduceCage(cage); switch (reduced.cells.length) { case 0: solveCage(cage); break; case 1: yield *setOnly(reduced.cells[0], reduced.total, "last cell left in " + math(cage) + " cage", cage); break; default: yield *addCageAndYield(reduced, "leftovers after solving cell"); } } // find the unsolved cells in a cage, and if possible return a new cage with those cells // it's possible if: 1) there are any unsolved cells at all, and 2) the cage op is + or x // returns { op: , total: , cells: } // if reduction is not possible, op and total are omitted function reduceCage(cage) { var reducible = "+x".includes(cage.op); var total = cage.total; var unsolvedCells = cage.cells.filter(function (c) { if (c.guess && reducible) total = opReduce(cage.op, total, c.guess); return !c.guess; }); var result = { cells: unsolvedCells }; if (reducible && unsolvedCells.length > 1) result.op = cage.op; if (reducible && unsolvedCells.length > 0) result.total = total; return result; } // if cells are all in the same row or column, return the line number // if they're not in line, return null // line number = row number for rows, boardSize + column number for columns function oneLine(cells) { var i = cells[0].i, j = cells[0].j; cells.forEach(function (cell) { if (i != null && cell.i != i) i = null; if (j != null && cell.j != j) j = null; }); // return a proper index into rowsAndColumns return i != null ? i : j != null ? boardSize + j : null; } function cellLines(cells) { var lines = []; cells.forEach(function(cell) { lines[cell.i] = true; lines[cell.j + boardSize] = true; }); var result = []; lines.forEach(function(line, i) { if (line) result.push(i); }); return result; } function cellInLine(cell, line) { return (line < boardSize ? cell.i == line : cell.j == line - boardSize); } function rowAndColumnPossibles() { var possibles = []; for (var i = 0; i < boardSize * 2; i++) { possibles[i] = pNew(boardSize); } return possibles; } // // MARK: convenient yield loops // // yield all unsolved cages of type op (to get all unsolved cages, use null op) function *yieldCages(op, fn) { yield *cages.yieldEach(function*(cage) { if (!cage.solved && (!op || cage.op == op)) { yield *fn(cage); } }); } // yield unsolved cells in all unsolved cages of type op (to get all unsolved cages, use null op) // fn params are (cage, cell, i), where i is index of cell in cage.cells function *yieldCageCells(op, fn) { yield *yieldCages(op, function*(cage) { yield *cage.cells.yieldEach(function*(cell, i) { if (!cell.guess) { yield *fn(cage, cell, i); } }); }); } // // MARK: solver rules // var rules = { "singleton": function*() { // if a cage has one cell, cell value is the cage total yield *yieldCages(null, function*(cage) { if (cage.cells.length == 1) { yield *setOnly(cage.cells[0], cage.total, "singleton cage"); } }); }, // TODO: rule that insures rows & columns don't contain values of solved cells "one or none": function*() { // if a cell has one possible value, solve it; if none, puzzle is broken yield *rows.yieldEach(function*(cells) { yield *cells.yieldEach(checkOneOrNone); }); }, "divisor": function*() { yield *yieldCageCells("x", function*(cage, cell) { var invalid = pValues(cell.possible, function(n) { return cage.total % n != 0; }); if (invalid.length > 0) yield *clear(cell, invalid, "not a divisor of " + cage.total, {highlight: cage.cells}); }); }, "addition": function*() { // eliminate values that make the rest of an addition cage impossible to complete yield *yieldCages("+", function*(cage) { if (cage.cells.length < 5) yield *removeCageKillers(cage); }); }, // TODO: refactor to eliminate repetition with subtraction "division": function*() { // eliminate values that can't complete a division cage yield *yieldCageCells("/", function*(cage, cell, i) { var values = []; var otherCell = cage.cells[1 - i]; pEach(cell.possible, function (n) { if (!pYes(otherCell.possible, [n * cage.total, n / cage.total])) { values.push(n); } }); if (values.length > 0) yield *clear(cell, values, "can't satisfy " + math(cage) + " with other cell", {highlight: cage.cells}); }); }, "multiplication": function*() { // eliminate values that can't complete a multiplication cage yield *yieldCages("x", function*(cage) { if (cage.cells.length < 5) yield *removeCageKillers(cage); }); }, "pigeonhole": function*() { // If possibility occurs only once in a row or column, it must appear there yield *rowsAndColumns.yieldEach(function*(cells, line) { var rowOrCol = line < boardSize ? "row" : "column"; for (var n = 1; n <= boardSize; n++) { var count = 0, target = null; cells.forEach(function (cell) { if (pYes(cell.possible, n)) { count++; target = cell; } }); if (count == 1 && !target.guess) { yield *setOnly(target, n, "only place left in " + rowOrCol + " for " + n, {cells: cells}); } } }); }, "subtraction": function*() { // Check legal subtraction possibilities yield* yieldCageCells('-', function*(cage, cell, i) { var values = []; var otherCell = cage.cells[1 - i]; pEach(cell.possible, function (n) { if (!pYes(otherCell.possible, [n + cage.total, n - cage.total])) { values.push(n); } }); if (values.length > 0) yield *clear(cell, values, "can't satisy " + math(cage) + " with other cell", {highlight: cage.cells}); }); }, "two pair": function*() { // If the possibilities of two cells in the same row or column all equal the same 2 // numbers, those two numbers must occupy those cells, and therefore aren't possible // in any other cells in the same row/column. yield *rowsAndColumns.yieldEach(function*(cells, line) { var rowOrCol = line < boardSize ? "row" : "column"; for (var i = 0; i < boardSize - 1; i++) { var cellA = cells[i]; if (pCount(cellA.possible) == 2) { for (var j = i + 1; j < boardSize; j++) { var cellB = cells[j]; if (cellB.possible.matches(cellA.possible)) { // two-pair found! remove these two values from all other cells var otherCells = arraySubtract(cells, [cellA, cellB]); var vals = pValues(cellA.possible); var why = "" + vals.join(", ") + " pair confined to two cells"; yield *otherCells.yieldEach(function*(cell) { yield *clear(cell, vals, why, {lit: [cellA, cellB], highlight: otherCells}); }); // is pair in same cage? cage bigger than 2? then make a subcage with leftover cells if (cellA.cage == cellB.cage && cages[cellA.cage].cells.length > 2) { var cage = cages[cellA.cage]; var subCage = { op: cage.op, total: opReduce(cage.op, cage.total, vals), cells: arraySubtract(cage.cells, [cellA, cellB]) }; yield *addCageAndYield(subCage, "leftovers after pair"); } } } } } }); }, "three": function*() { // If the possibilities of three cells in the same row or column all equal the same 3 // numbers, those three numbers must occupy those cells, and therefore aren't possible // in any other cells in the same row/column. yield *rowsAndColumns.yieldEach(function*(cells, line) { var rowOrCol = line < boardSize ? "row" : "column"; for (var i = 0; i < boardSize - 2; i++) { var cellA = cells[i]; if (cellA.guess || pCount(cellA.possible) > 3) continue; for (var j = i + 1; j < boardSize - 1; j++) { var cellB = cells[j]; if (cellB.guess || pCount(cellB.possible) > 3) continue; var possibleAB = pUnion(cellA.possible, cellB.possible); if (pCount(possibleAB) > 3) continue; for (var k = j + 1; k < boardSize; k++) { var cellC = cells[k]; if (cellC.guess || pCount(cellC.possible) > 3) continue; var possibleABC = pUnion(possibleAB, cellC.possible); if (pCount(possibleABC) == 3) { // threesome found! remove these three values from all other cells var otherCells = arraySubtract(cells, [cellA, cellB, cellC]); var vals = pValues(possibleABC); var why = "" + vals[0] + " " + vals[1] + " " + vals[2] + " triplet confined to three cells"; yield *otherCells.yieldEach(function*(cell) { yield *clear(cell, vals, why, {highlight: otherCells, lit: [cellA, cellB, cellC]}); }); // is threesome in same cage? cage bigger than 3? then make a subcage with leftover cells if (cellA.cage == cellB.cage && cellA.cage == cellC.cage && cages[cellA.cage].cells.length > 3) { var cage = cages[cellA.cage]; var subCage = { op: cage.op, total: opReduce(cage.op, cage.total, vals), cells: arraySubtract(cage.cells, [cellA, cellB, cellC]) }; yield *addCageAndYield(subCage, "leftovers after threesome"); } } } } } }); }, "two and two": function*() { // if a value must occupy either column A or B in two different rows, // eliminate that value in all other rows of A and B var pairs = []; yield *[rows, columns].yieldEach(function*(lines) { var linesAreRows = lines == rows; var crossers = linesAreRows ? columns : rows; var lineLabel = linesAreRows ? "row" : "column"; var crosserLabel = linesAreRows ? "column" : "row"; // reset pairs for (var n = 1; n <= boardSize; n++) pairs[n] = []; // scan lines for pairs lines.forEach(function (cells, line) { // count how many cells each value is possible in for (var n = 1; n <= boardSize; n++) { var cellsWithValue = []; cells.forEach(function (cell, i) { if (pYes(cell.possible, n)) { if (cellsWithValue.length < 3) cellsWithValue.push(i); // don't collect past 3 occurrences } }); // if only two cells have this value, it's a pair! save it if (cellsWithValue.length == 2) pairs[n].push({line: line, slots: cellsWithValue}); } }); // any two line pairs share the same crossers? for (n = 1; n <= boardSize; n++) { if (pairs[n].length > 1) { // look for a match for (var j = 0; j < pairs[n].length - 1; j++) { for (var k = j + 1; k < pairs[n].length; k++) { var pairA = pairs[n][j], pairB = pairs[n][k]; if (pairA.slots.matches(pairB.slots)) { // found one! var i1 = pairA.line, i2 = pairB.line, j1 = pairA.slots[0], j2 = pairA.slots[1]; var foursome = linesAreRows ? [board[i1][j1], board[i1][j2], board[i2][j1], board[i2][j2]] : [board[j1][i1], board[j1][i2], board[j2][i1], board[j2][i2]]; var why = "only two spots for " + n + " in both " + lineLabel + "s, clear rest of " + crosserLabel; var softLit = lines[pairA.line].concat(lines[pairB.line]); yield *pairA.slots.yieldEach(function*(pairSlot) { yield *crossers[pairSlot].yieldEach(function*(cell) { var thisLine = linesAreRows ? cell.i : cell.j; var highlight = linesAreRows ? columns[cell.j] : rows[cell.i]; if (thisLine != pairA.line && thisLine != pairB.line) { yield *clear(cell, n, why, {lit: foursome, softLit: softLit, highlight: highlight}); } }); }); } } } } } }); }, "must-have divisor": function*() { var n = boardSize; var mustHaveDivisors = n < 6 ? [3, 5] : n > 6 ? [5, 7] : [5]; yield *yieldCageCells('x', function*(cage) { if (cage.line != null) { var ui = {lit: cage.cells, highlight: rowsAndColumns[cage.line]}; yield *mustHaveDivisors.yieldEach(function*(d) { if (cage.total % d == 0) { var why = math(cage) + " cage needs the " + d; yield *rowsAndColumns[cage.line].yieldEach(function*(cell) { if (!cage.cells.contains(cell)) yield *clear(cell, d, why, ui); }); } }); }}); }, "must have in line": function*() { var lineValues = pNew(boardSize); // check all cages of less than 4 cells yield *yieldCages(null, function*(cage) { if (cage.cells.length < 4) { // first remove the cage killers yield *removeCageKillers(cage); var allValues = rowAndColumnPossibles(); // values possible in each line var lines = cellLines(cage.cells); // lines covered by this cage eachSolution(cage, function(solution) { // check each cage solution lines.forEach(function(line) { // for each line in the solution pClearAll(lineValues); cage.cells.forEach(function(cell, i) { if (cellInLine(cell, line)) { pSet(lineValues, solution[i]); } }); allValues[line] = pIntersect(allValues[line], lineValues); }) }); yield *lines.yieldEach(function*(line) { if (pCount(allValues[line]) > 0) { var rowOrCol = line < boardSize ? "row" : "column"; var why = math(cage) + " cage needs the " + pString(allValues[line]) + " in this " + rowOrCol; var ui = {lit: cage.cells, highlight: rowsAndColumns[line]}; yield *rowsAndColumns[line].yieldEach(function*(cell) { if (!cage.cells.contains(cell)) yield *clear(cell, pValues(allValues[line]), why, ui); }); $scope.lit = cage; $scope.cursorShadow = rowsAndColumns[line]; } }); }}); }, "line sum": function*() { yield *rowsAndColumns.yieldEach(function*(cells, line) { var rowOrColumn = line < boardSize ? "row" : "column"; var why = "remainder of " + rowOrColumn + " sum"; var remainder = rowTotal; for (var i = 0; i < cells.length; i++) { var cell = cells[i], cage = cages[cell.cage]; if (cage.op == '+' && cage.line == line) { remainder -= cage.total; cells = arraySubtract(cells, cage.cells); i -= 1; // adjust after cells are dropped } else if (cell.guess) { remainder -= cell.guess; cells = arraySubtract(cells, [cell]); i -= 1; } } if (cells.length == 1) { yield *setOnly(cells[0], remainder, why); } else if (cells.length > 1 && cells.length <= boardSize / 2) { yield *addCageAndYield({ op: '+', total: remainder, cells: cells }, why); } }); }, "line product": function*() { yield *rowsAndColumns.yieldEach(function*(cells, line) { var rowOrColumn = line < boardSize ? "row" : "column"; var why = "remainder of " + rowOrColumn + " product"; var remainder = rowProduct; for (var i = 0; i < cells.length; i++) { var cell = cells[i], cage = cages[cell.cage]; if (cage.op == 'x' && cage.line == line) { remainder /= cage.total; cells = arraySubtract(cells, cage.cells); i -= 1; // adjust after cells are dropped } else if (cell.guess) { remainder /= cell.guess; cells = arraySubtract(cells, [cell]); i -= 1; } } if (cells.length == 1) { yield *setOnly(cells[0], remainder, why); } else if (cells.length > 1 && cells.length <= boardSize / 2) { yield *addCageAndYield({ op: 'x', total: remainder, cells: cells }, why); } }); }, "double math": function*() { // if two cages have the same cells but different math, there is likely only one set of numbers that works var valid = pNew(boardSize); yield *yieldCages(null, function*(cage) { if (!cage.collisionChecked) { yield *yieldCages(null, function*(otherCage) { if (cage != otherCage && cage.cells.matches(otherCage.cells)) { // found two cages with same cells! now identify the values that can satisfy both maths pClearAll(valid); eachSolution(cage, function(solution) { if (valuesWork(otherCage.op, otherCage.total, solution)) pSet(valid, solution); }); // restrict all cells to those values var why = "only " + pValues(valid).join(" ") + " satisfy both " + math(cage) + " and " + math(otherCage); var invalidValues = pValues(pInvert(valid)); yield *cage.cells.yieldEach(function*(cell) { if (!cell.guess) { yield *clear(cell, invalidValues, why, {highlight: cage.cells}); }}) }}); cage.collisionChecked = true; }}); }, "subtract subcage": function*() { var valid = pNew(boardSize); yield *yieldCages(null, function*(cage) { if (!cage.subcageChecked) { yield *yieldCages(null, function*(otherCage) { if (cage.op == otherCage.op && subsets(cage.cells, otherCage.cells)) { // found subcage! var superCage = cage.cells.length > otherCage.cells.length ? cage : otherCage; var subCage = superCage == cage ? otherCage : cage; var total = opReduce(cage.op, superCage.total, subCage.total); var cells = arraySubtract(superCage.cells, subCage.cells); var newCage = { op: cage.op, total: total, cells: cells}; var why = math(subCage) + " from " + math(superCage) + " leaves " + math(newCage); yield *addCageAndYield(newCage, why); } }); cage.subcageChecked = true; }}); }, // TODO fix messaging; some values are cleared because no solution contains them, not due to 'not both' rule "not both": function*() { // if a cell has only two possible values, no inline cage in its row or column can use both of those values yield *rows.yieldEach(function*(cells) { yield *cells.yieldEach(function*(cell) { if (pCount(cell.possible) == 2) { var vals = pValues(cell.possible); // check inline cages in this line yield *yieldCages(null, function*(cage) { if (cage.line == cell.i || cage.line == cell.j + boardSize) { var rowOrCol = cell.line < boardSize ? "row" : "column"; if (!cage.cells.contains(cell)) { // first kill solutions that just don't work yield *removeCageKillers(cage); // then find solutions that use both var validSolutions = []; eachSolution(cage, function (solution) { if (!solution.contains(vals[0]) || !solution.contains(vals[1])) { // solution doesn't use both values! it's valid validSolutions.push(solution); } }); var possible = pNew(boardSize); yield *cage.cells.yieldEach(function*(cageCell, i) { pClearAll(possible); validSolutions.forEach(function (solution) { pSet(possible, solution[i]); }); var why = "other cell needs " + vals.join(" or ") + ", cage can't use both"; var ui = {highlight: cage.cells, lit: [cell]}; yield *clear(cageCell, pValues(pInvert(possible)), why, ui); }); } } }); } })}); } }; return solver; }; function math(cage) { return "" + cage.total + cage.op; } // finds valid solutions to the cage and fires a callback function for each one function eachSolution(cage, fn) { var boardSize = pSize(cage.cells[0].possible); f(cage.total, cage.cells, fn, []); function f(total, cells, fn, acc) { if (cells.length == 1) { if (pYes(cells[0].possible, total)) fn(acc.concat([total])); } else { var restOfCells = cells.slice(1); pEach(cells[0].possible, function(n) { if (opLegal(cage.op, total, n, boardSize)) { var newTotals = opReduce(cage.op, total, n); if (!(newTotals instanceof Array)) newTotals = [newTotals]; newTotals.forEach(function(newTotal) { f(newTotal, restOfCells, fn, acc.concat([n])); }); } }); } } } function countSolutions(board, cages, count) { if (count === undefined) count = 0; if (cages.length == 0) { return count + 1; } var cage = cages[0]; var restOfCages = cages.slice(1); var solved = true; for (var c = 0; c < cage.cells.length; c++) { var i = cage.cells[c][0], j = cage.cells[c][1], cell = board[i][j]; if (pCount(cell.possible) == 0) return count; // not a solution! return without increasing count if (pCount(cell.possible) != 1) solved = false; // mark as unsolved } if (solved) { // if this cage solved, go to next return countSolutions(board, restOfCages, count); } else { eachSolution2(cage.op, cage.total, cage.cells, board, function(solution) { var boardCopy = angular.copy(board); setSolution(boardCopy, cage, solution); count = countSolutions(boardCopy, restOfCages, count); }); } return count; } function eachSolution2(op, total, cells, board, fn, acc) { if (acc === undefined) acc = []; var ij = cells[0], i = ij[0], j = ij[1], cell = board[i][j]; if (cells.length == 1) { if (pYes(cell.possible, total)) fn(acc.concat([total])); } else { var restOfCells = cells.slice(1); pEach(cell.possible, function (n) { if (opLegal(op, total, n, board.length)) { var newTotals = opReduce(op, total, n); if (!(newTotals instanceof Array)) newTotals = [newTotals]; newTotals.forEach(function (newTotal) { eachSolution2(op, newTotal, restOfCells, board, fn, acc.concat([n])); }); } }); } } function setSolution(board, cage, solution) { cage.cells.forEach(function(coords, c) { var i = coords[0], j = coords[1], cell = board[i][j], n = solution[c]; if (!cell.guess) { pSetOnly(cell.possible, n); for (var ii = 0; ii < board.length; ii++) if (ii != i) pClear(board[ii][j].possible, n); for (var jj = 0; jj < board.length; jj++) if (jj != j) pClear(board[i][jj].possible, n); } }); } // TODO do pUnion and pIntersect in-place function allPossible(cells) { if (cells.length == 0) return null; var p = angular.copy(cells[0].possible); for (var i = 1; i < cells.length; i++) p = pUnion(p, cells[i].possible); return p; } function opLegal(op, total, n, boardSize) { if (op == "+") return total > n; if (op == "x") return total % n == 0; if (op == "-") return n - total > 0 || total + n <= boardSize; if (op == "/") return n % total == 0 || total * n <= boardSize; } function doOp(op, a, b) { if (op == "+") return a + b; if (op == "x") return a * b; if (op == "-") return [a - b, b - a].filter(function(x) { return x > 0; }); if (op == "/") return [a % b == 0 ? a / b : false, b % a == 0 ? b / a : false].filter(function(x) { return x; }); } function opReduce(op, total, n) { if (n instanceof Array) { n.forEach(function(nn) { total = opReduce(op, total, nn); }); return total; } if (op == "+") return total - n; if (op == "x") return total / n; if (op == "-") return [n + total, n - total].filter(function(x) { return x > 0; }); if (op == "/") return [n % total == 0 ? n / total : false, n * total].filter(function(x) { return x; }); } // returns true if performing the operation on the values produces the total function valuesWork(op, total, values) { if (op == "/") return values[0] / values[1] == total || values[1] / values[0] == total; if (op == "-") return Math.abs(values[0] - values[1]) == total; if (op == "+" || op == "x") { var result = op == "+" ? 0 : 1; values.forEach(function (v) { result = op == "+" ? result + v : result * v; }); return result == total; } return false; } // // MARK: tracking possible values // // to track possible values we use an array p of length n, where n is the size of the board // p[i] == true if i is a possible value in the cell, false otherwise // p[0] stores the count of possible values // the following functions maintain this function pNew(n, fill) { if (fill === undefined) fill = true; var p = [n]; for (var i = 1; i <= n; i++) p[i] = fill; return p; } function pSetAll(p) { for (var i = 1; i < p.length; i++) p[i] = true; p[0] = p.length - 1; } function pClearAll(p) { for (var i = 1; i < p.length; i++) p[i] = false; p[0] = 0; } function pSet(p, x) { if (x instanceof Array) x.forEach(function(k) { pSet(p, k); }); else if (x > 0 && x < p.length && !p[x]) { p[x] = true; p[0]++; } } function pClear(p, x) { if (x instanceof Array) x.forEach(function(k) { pClear(p, k); }); else if (x > 0 && x < p.length && p[x]) { p[x] = false; p[0]--; } } function pSetOnly(p, x) { pClearAll(p); pSet(p, x); } function pYes(p, x) { if (x instanceof Array) { for (var i = 0; i < x.length; i++) if (pYes(p, x[i])) return true; return false; } else { return (x > 0 && x < p.length && p[x]); } } function pCount(p) { return p[0]; } function pSize(p) { return p.length - 1; } function pEach(p, fn) { for (var i = 1; i < p.length; i++) if (p[i]) fn(i); } function pValues(p, fn) { // fn is an optional filtering function var values = []; pEach(p, function(i) { if (!fn || fn(i)) values.push(i); }); return values; } function pFirstValue(p) { return p.indexOf(true); } function pUnion(a, b) { var p = pNew(Math.max(pSize(a), pSize(b))); for (var i = 1; i < p.length; i++) if (!a[i] && !b[i]) pClear(p, i); return p; } function pIntersect(a, b) { var p = pNew(Math.min(pSize(a), pSize(b))); for (var i = 1; i < p.length; i++) if (!a[i] || !b[i]) pClear(p, i); return p; } function pInvert(p) { var q = []; q[0] = (p.length - 1) - p[0]; for (var i = 1; i < p.length; i++) q[i] = !p[i]; return q; } function pString(p) { return pValues(p).join(""); } // // MARK: convenience functions // // string for describing a cell in console output function cellName(cell) { return "(" + cell.i + "," + cell.j + ")"; } // string for describing a cage in console output function cageName(cage) { var name = "[" + math(cage); cage.cells.forEach(function(cell, i) { name += (i > 0 ? "," : " ") + cellName(cell); }); cage.cells.forEach(function(cell, i) { name += (i > 0 ? "," : " ") + pString(cell.possible); }); return name + "]"; } // // MARK: utility functions // // sum of integers from 1 to n function arithSum(n) { return (n + 1) * n / 2 } // product of integers from 1 to n function factorial(n) { if (n < 2) return 1; else return n * factorial(n - 1); } function arraySubtract(a, b) { var result = []; a.forEach(function(elem) { if (b.indexOf(elem) == -1) result.push(elem); }); return result; } function rowsToColumns(a) { var columns = []; for (var j = 0; j < a[0].length; j++) { columns[j] = []; for (var i = 0; i < a.length; i++) { columns[j].push(a[i][j]); } } return columns; } // returns true if the arrays are different lengths and the smaller is a subset of the larger function subsets(a, b) { if (a.length == b.length) return false; // strict subsets, not same set var big = a.length > b.length ? a : b; var lil = a.length > b.length ? b : a; for (var i = 0; i < lil.length; i++) { if (big.indexOf(lil[i]) < 0) return false; } return true; } function intersection(a, b) { var result = []; for (var i = 0; i < a.length; i++) { var item = a[i]; if (!result.contains(item) && b.contains(item)) result.push(item); } return result; } Array.prototype.yieldEach = function*(fn) { for (var i = 0; i < this.length; i++) yield* fn(this[i], i); }; Array.prototype.matches = function(b) { if (b.length != this.length) return false; for (var i = 0; i < this.length; i++) { if (b[i] != this[i]) return false; } return true; }; if (!Array.prototype.contains) { Array.prototype.contains = function(item) { return this.indexOf(item) != -1; } } Object.prototype.extend = function(b) { var result = {}, key = null; for (key in this) if (this.hasOwnProperty(key)) result[key] = this[key]; if (b) for (key in b) if (b.hasOwnProperty(key)) result[key] = b[key]; return result; } });