From 0929719a845897cc8567cf972e07a69a71f0fa6f Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 30 Nov 2023 13:29:08 -0500 Subject: Migrate to a full rails app --- app/assets/javascripts/solve.js | 531 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 531 insertions(+) create mode 100644 app/assets/javascripts/solve.js (limited to 'app/assets/javascripts/solve.js') diff --git a/app/assets/javascripts/solve.js b/app/assets/javascripts/solve.js new file mode 100644 index 0000000..8695291 --- /dev/null +++ b/app/assets/javascripts/solve.js @@ -0,0 +1,531 @@ +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