namespace(function() { // @Volatile -- must match order of MOVE_* in trace2 // Move these, dummy. var PATH_NONE = 0 var PATH_LEFT = 1 var PATH_RIGHT = 2 var PATH_TOP = 3 var PATH_BOTTOM = 4 window.MAX_SOLUTIONS = 0 var solutionPaths = [] var asyncTimer = 0 var task = null var puzzle = null var path = [] var SOLVE_SYNC = false var SYNC_THRESHOLD = 9 // Depth at which we switch to a synchronous solver (for perf) var doPruning = false var percentages = [] var NODE_DEPTH = 9 var nodes = 0 function countNodes(x, y, depth) { // Check for collisions (outside, gap, self, other) var cell = puzzle.getCell(x, y) if (cell == null) return if (cell.gap > window.GAP_NONE) return if (cell.line !== window.LINE_NONE) return if (puzzle.symType == SYM_TYPE_NONE) { puzzle.updateCell2(x, y, 'line', window.LINE_BLACK) } else { var sym = puzzle.getSymmetricalPos(x, y) if (puzzle.matchesSymmetricalPos(x, y, sym.x, sym.y)) return // Would collide with our reflection var symCell = puzzle.getCell(sym.x, sym.y) if (symCell.gap > window.GAP_NONE) return puzzle.updateCell2(x, y, 'line', window.LINE_BLUE) puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_YELLOW) } if (depth < NODE_DEPTH) { nodes++ if (y%2 === 0) { countNodes(x - 1, y, depth + 1) countNodes(x + 1, y, depth + 1) } if (x%2 === 0) { countNodes(x, y - 1, depth + 1) countNodes(x, y + 1, depth + 1) } } tailRecurse(x, y) } // Generates a solution via DFS recursive backtracking window.solve = function(p, partialCallback, finalCallback) { if (task != null) throw Error('Cannot start another solve() while one is already in progress') var start = (new Date()).getTime() puzzle = p var startPoints = [] var numEndpoints = 0 puzzle.hasNegations = false puzzle.hasPolyominos = false for (var x=0; x 0) { // Tasks are pushed in order. To do DFS, we need to enqueue them in reverse order. for (var i=newTasks.length - 1; i >= 0; i--) { task = { 'code': newTasks[i], 'nextTask': task, } } } // Asynchronizing is expensive. As such, we don't want to do it too often. // However, we would like 'cancel solving' to be responsive. So, we call setTimeout every so often. var doAsync = false if (!SOLVE_SYNC) { doAsync = (asyncTimer++ % 100 === 0) while (nodes >= percentages[0]) { if (partialCallback) partialCallback(100 - percentages.length) percentages.shift() doAsync = true } } if (doAsync) { setTimeout(function() { taskLoop(partialCallback, finalCallback) }, 0) } else { taskLoop(partialCallback, finalCallback) } } function tailRecurse(x, y) { // Tail recursion: Back out of this cell puzzle.updateCell2(x, y, 'line', window.LINE_NONE) if (puzzle.symType != SYM_TYPE_NONE) { var sym = puzzle.getSymmetricalPos(x, y) puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_NONE) } } // @Performance: This is the most central loop in this code. // Any performance efforts should be focused here. // Note: Most mechanics are NP (or harder), so don't feel bad about solving them by brute force. // https://arxiv.org/pdf/1804.10193.pdf function solveLoop(x, y, numEndpoints, earlyExitData) { // Stop trying to solve once we reach our goal if (solutionPaths.length >= window.MAX_SOLUTIONS) return // Check for collisions (outside, gap, self, other) var cell = puzzle.getCell(x, y) if (cell == null) return if (cell.gap > window.GAP_NONE) return if (cell.line !== window.LINE_NONE) return if (puzzle.symType == SYM_TYPE_NONE) { puzzle.updateCell2(x, y, 'line', window.LINE_BLACK) } else { var sym = puzzle.getSymmetricalPos(x, y) if (puzzle.matchesSymmetricalPos(x, y, sym.x, sym.y)) return // Would collide with our reflection var symCell = puzzle.getCell(sym.x, sym.y) if (symCell.gap > window.GAP_NONE) return puzzle.updateCell2(x, y, 'line', window.LINE_BLUE) puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_YELLOW) } if (path.length < NODE_DEPTH) nodes++ if (cell.end != null) { path.push(PATH_NONE) puzzle.endPoint = {'x': x, 'y': y} var puzzleData = window.validate(puzzle, true) if (puzzleData.valid()) solutionPaths.push(path.slice()) path.pop() // If there are no further endpoints, tail recurse. // Otherwise, keep going -- we might be able to reach another endpoint. numEndpoints-- if (numEndpoints === 0) return tailRecurse(x, y) } var newEarlyExitData = null if (doPruning) { var isEdge = x <= 0 || y <= 0 || x >= puzzle.width - 1 || y >= puzzle.height - 1 newEarlyExitData = [ earlyExitData[0] || (!isEdge && earlyExitData[2].isEdge), // Have we ever left an edge? earlyExitData[2], // The position before our current one {'x':x, 'y':y, 'isEdge':isEdge} // Our current position. ] if (earlyExitData[0] && !earlyExitData[1].isEdge && earlyExitData[2].isEdge && isEdge) { // See the above comment for an explanation of this math. var floodX = earlyExitData[2].x + (earlyExitData[1].x - x) var floodY = earlyExitData[2].y + (earlyExitData[1].y - y) var region = puzzle.getRegion(floodX, floodY) if (region != null) { var regionData = window.validateRegion(puzzle, region, true) if (!regionData.valid()) return tailRecurse(x, y) // Additionally, we might have left an endpoint in the enclosed region. // If so, we should decrement the number of remaining endpoints (and possibly tail recurse). for (var pos of region) { var endCell = puzzle.grid[pos.x][pos.y] if (endCell != null && endCell.end != null) numEndpoints-- } if (numEndpoints === 0) return tailRecurse(x, y) } } } if (SOLVE_SYNC || path.length > SYNC_THRESHOLD) { path.push(PATH_NONE) // Recursion order (LRUD) is optimized for BL->TR and mid-start puzzles if (y%2 === 0) { path[path.length-1] = PATH_LEFT solveLoop(x - 1, y, numEndpoints, newEarlyExitData) path[path.length-1] = PATH_RIGHT solveLoop(x + 1, y, numEndpoints, newEarlyExitData) } if (x%2 === 0) { path[path.length-1] = PATH_TOP solveLoop(x, y - 1, numEndpoints, newEarlyExitData) path[path.length-1] = PATH_BOTTOM solveLoop(x, y + 1, numEndpoints, newEarlyExitData) } path.pop() tailRecurse(x, y) } else { // Push a dummy element on the end of the path, so that we can fill it correctly as we DFS. // This element is popped when we tail recurse (which always happens *after* all of our DFS!) path.push(PATH_NONE) // Recursion order (LRUD) is optimized for BL->TR and mid-start puzzles var newTasks = [] if (y%2 === 0) { newTasks.push(function() { path[path.length-1] = PATH_LEFT return solveLoop(x - 1, y, numEndpoints, newEarlyExitData) }) newTasks.push(function() { path[path.length-1] = PATH_RIGHT return solveLoop(x + 1, y, numEndpoints, newEarlyExitData) }) } if (x%2 === 0) { newTasks.push(function() { path[path.length-1] = PATH_TOP return solveLoop(x, y - 1, numEndpoints, newEarlyExitData) }) newTasks.push(function() { path[path.length-1] = PATH_BOTTOM return solveLoop(x, y + 1, numEndpoints, newEarlyExitData) }) } newTasks.push(function() { path.pop() tailRecurse(x, y) }) return newTasks } } window.cancelSolving = function() { console.info('Cancelled solving') window.MAX_SOLUTIONS = 0 // Causes all new solveLoop calls to exit immediately. tasks = [] } // Only modifies the puzzle object (does not do any graphics updates). Used by metapuzzle.js to determine subpuzzle polyshapes. window.drawPathNoUI = function(puzzle, path) { puzzle.clearLines() // Extract the start data from the first path element var x = path[0].x var y = path[0].y var cell = puzzle.getCell(x, y) if (cell == null || cell.start !== true) throw Error('Path does not begin with a startpoint: ' + JSON.stringify(cell)) for (var i=1; i