diff options
Diffstat (limited to 'app/assets/javascripts/solve.js')
-rw-r--r-- | app/assets/javascripts/solve.js | 531 |
1 files changed, 531 insertions, 0 deletions
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 @@ | |||
1 | namespace(function() { | ||
2 | |||
3 | // @Volatile -- must match order of MOVE_* in trace2 | ||
4 | // Move these, dummy. | ||
5 | var PATH_NONE = 0 | ||
6 | var PATH_LEFT = 1 | ||
7 | var PATH_RIGHT = 2 | ||
8 | var PATH_TOP = 3 | ||
9 | var PATH_BOTTOM = 4 | ||
10 | |||
11 | window.MAX_SOLUTIONS = 0 | ||
12 | var solutionPaths = [] | ||
13 | var asyncTimer = 0 | ||
14 | var task = null | ||
15 | var puzzle = null | ||
16 | var path = [] | ||
17 | var SOLVE_SYNC = false | ||
18 | var SYNC_THRESHOLD = 9 // Depth at which we switch to a synchronous solver (for perf) | ||
19 | var doPruning = false | ||
20 | |||
21 | var percentages = [] | ||
22 | var NODE_DEPTH = 9 | ||
23 | var nodes = 0 | ||
24 | function countNodes(x, y, depth) { | ||
25 | // Check for collisions (outside, gap, self, other) | ||
26 | var cell = puzzle.getCell(x, y) | ||
27 | if (cell == null) return | ||
28 | if (cell.gap > window.GAP_NONE) return | ||
29 | if (cell.line !== window.LINE_NONE) return | ||
30 | |||
31 | if (puzzle.symType == SYM_TYPE_NONE) { | ||
32 | puzzle.updateCell2(x, y, 'line', window.LINE_BLACK) | ||
33 | } else { | ||
34 | var sym = puzzle.getSymmetricalPos(x, y) | ||
35 | if (puzzle.matchesSymmetricalPos(x, y, sym.x, sym.y)) return // Would collide with our reflection | ||
36 | |||
37 | var symCell = puzzle.getCell(sym.x, sym.y) | ||
38 | if (symCell.gap > window.GAP_NONE) return | ||
39 | |||
40 | puzzle.updateCell2(x, y, 'line', window.LINE_BLUE) | ||
41 | puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_YELLOW) | ||
42 | } | ||
43 | |||
44 | if (depth < NODE_DEPTH) { | ||
45 | nodes++ | ||
46 | |||
47 | if (y%2 === 0) { | ||
48 | countNodes(x - 1, y, depth + 1) | ||
49 | countNodes(x + 1, y, depth + 1) | ||
50 | } | ||
51 | |||
52 | if (x%2 === 0) { | ||
53 | countNodes(x, y - 1, depth + 1) | ||
54 | countNodes(x, y + 1, depth + 1) | ||
55 | } | ||
56 | } | ||
57 | |||
58 | tailRecurse(x, y) | ||
59 | } | ||
60 | |||
61 | // Generates a solution via DFS recursive backtracking | ||
62 | window.solve = function(p, partialCallback, finalCallback) { | ||
63 | if (task != null) throw Error('Cannot start another solve() while one is already in progress') | ||
64 | var start = (new Date()).getTime() | ||
65 | |||
66 | puzzle = p | ||
67 | var startPoints = [] | ||
68 | var numEndpoints = 0 | ||
69 | puzzle.hasNegations = false | ||
70 | puzzle.hasPolyominos = false | ||
71 | for (var x=0; x<puzzle.width; x++) { | ||
72 | for (var y=0; y<puzzle.height; y++) { | ||
73 | var cell = puzzle.grid[x][y] | ||
74 | if (cell == null) continue | ||
75 | if (cell.start === true) { | ||
76 | startPoints.push({'x': x, 'y': y}) | ||
77 | } | ||
78 | if (cell.end != null) numEndpoints++ | ||
79 | if (cell.type == 'nega') puzzle.hasNegations = true | ||
80 | if (cell.type == 'poly' || cell.type == 'ylop') puzzle.hasPolyominos = true | ||
81 | } | ||
82 | } | ||
83 | |||
84 | // Puzzles which are small enough should be solved synchronously, since the cost of asynchronizing | ||
85 | // is greater than the cost of the puzzle. | ||
86 | SOLVE_SYNC = false | ||
87 | if (puzzle.symType != SYM_TYPE_NONE) { // 5x5 is the max for symmetry puzzles | ||
88 | if (puzzle.width * puzzle.height <= 121) SOLVE_SYNC = true | ||
89 | } else if (puzzle.pillar === true) { // 4x5 is the max for non-symmetry, pillar puzzles | ||
90 | if (puzzle.width * puzzle.height <= 108) SOLVE_SYNC = true | ||
91 | } else { // 5x5 is the max for non-symmetry, non-pillar puzzles | ||
92 | if (puzzle.width * puzzle.height <= 121) SOLVE_SYNC = true | ||
93 | } | ||
94 | console.log('Puzzle is a', puzzle.width, 'by', puzzle.height, 'solving ' + (SOLVE_SYNC ? 'sync' : 'async')) | ||
95 | |||
96 | // We pre-traverse the grid (only considering obvious failure states like going out of bounds), | ||
97 | // and compute a total number of nodes that are reachable within some NODE_DEPTH steps. | ||
98 | // Then, during actual traversal, we can compare the number of nodes reached to the precomputed count | ||
99 | // to get a (fairly accurate) progress bar. | ||
100 | for (var pos of startPoints) { | ||
101 | countNodes(pos.x, pos.y, 0) | ||
102 | } | ||
103 | console.log('Pretraversal found', nodes, 'nodes') | ||
104 | percentages = [] | ||
105 | for (var i=0; i<100; i++) { | ||
106 | percentages.push(Math.floor(i * nodes / 100)) | ||
107 | } | ||
108 | nodes = 0 | ||
109 | |||
110 | solutionPaths = [] | ||
111 | // Some reasonable default data, which will avoid crashes during the solveLoop. | ||
112 | var earlyExitData = [false, {'isEdge': false}, {'isEdge': false}] | ||
113 | if (window.MAX_SOLUTIONS === 0) window.MAX_SOLUTIONS = 10000 | ||
114 | |||
115 | // Large pruning optimization -- Attempt to early exit once we cut out a region. | ||
116 | // Inspired by https://github.com/Overv/TheWitnessSolver | ||
117 | // For non-pillar puzzles, every time we draw a line from one edge to another, we cut out two regions. | ||
118 | // We can detect this by asking if we've ever left an edge, and determining if we've just touched an edge. | ||
119 | // However, just touching the edge isn't sufficient, since we could still enter either region. | ||
120 | // As such, we wait one additional step, to see which half we have moved in to, then we evaluate | ||
121 | // whichever half you moved away from (since you can no longer re-enter it). | ||
122 | // | ||
123 | // Consider this pathway (tracing X-X-X-A-B-C). | ||
124 | // ....X.... | ||
125 | // . . X . . | ||
126 | // ....X.... | ||
127 | // . . A . . | ||
128 | // ...CB.... | ||
129 | // | ||
130 | // Note that, once we have reached B, the puzzle is divided in half. However, we could go either | ||
131 | // left or right -- so we don't know which region is safe to validate. | ||
132 | // Once we reach C, however, the region to the right is closed off. | ||
133 | // As such, we can start a flood fill from the cell to the right of A, computed by A+(C-B). | ||
134 | // | ||
135 | // Unfortunately, this optimization doesn't work for pillars, since the two regions are still connected. | ||
136 | // Additionally, this optimization doesn't work when custom mechanics are active, as many custom mechanics | ||
137 | // depend on the path through the entire puzzle | ||
138 | doPruning = (puzzle.pillar === false && !puzzle.settings.CUSTOM_MECHANICS) | ||
139 | |||
140 | task = { | ||
141 | 'code': function() { | ||
142 | var newTasks = [] | ||
143 | |||
144 | for (var pos of startPoints) { | ||
145 | // ;(function(a){}(a)) | ||
146 | // This syntax is used to forcibly copy arguments which are otherwise part of the loop. | ||
147 | // Note that we don't need to copy objects, just value types. | ||
148 | ;(function(pos) { | ||
149 | newTasks.push(function() { | ||
150 | path = [pos] | ||
151 | puzzle.startPoint = pos | ||
152 | return solveLoop(pos.x, pos.y, numEndpoints, earlyExitData) | ||
153 | }) | ||
154 | }(pos)) | ||
155 | } | ||
156 | return newTasks | ||
157 | } | ||
158 | } | ||
159 | |||
160 | taskLoop(partialCallback, function() { | ||
161 | var end = (new Date()).getTime() | ||
162 | console.log('Solved', puzzle, 'in', (end-start)/1000, 'seconds') | ||
163 | if (finalCallback) finalCallback(solutionPaths) | ||
164 | }) | ||
165 | return solutionPaths | ||
166 | } | ||
167 | |||
168 | function taskLoop(partialCallback, finalCallback) { | ||
169 | if (task == null) { | ||
170 | finalCallback() | ||
171 | return | ||
172 | } | ||
173 | |||
174 | var newTasks = task.code() | ||
175 | task = task.nextTask | ||
176 | if (newTasks != null && newTasks.length > 0) { | ||
177 | // Tasks are pushed in order. To do DFS, we need to enqueue them in reverse order. | ||
178 | for (var i=newTasks.length - 1; i >= 0; i--) { | ||
179 | task = { | ||
180 | 'code': newTasks[i], | ||
181 | 'nextTask': task, | ||
182 | } | ||
183 | } | ||
184 | } | ||
185 | |||
186 | // Asynchronizing is expensive. As such, we don't want to do it too often. | ||
187 | // However, we would like 'cancel solving' to be responsive. So, we call setTimeout every so often. | ||
188 | var doAsync = false | ||
189 | if (!SOLVE_SYNC) { | ||
190 | doAsync = (asyncTimer++ % 100 === 0) | ||
191 | while (nodes >= percentages[0]) { | ||
192 | if (partialCallback) partialCallback(100 - percentages.length) | ||
193 | percentages.shift() | ||
194 | doAsync = true | ||
195 | } | ||
196 | } | ||
197 | |||
198 | if (doAsync) { | ||
199 | setTimeout(function() { | ||
200 | taskLoop(partialCallback, finalCallback) | ||
201 | }, 0) | ||
202 | } else { | ||
203 | taskLoop(partialCallback, finalCallback) | ||
204 | } | ||
205 | } | ||
206 | |||
207 | function tailRecurse(x, y) { | ||
208 | // Tail recursion: Back out of this cell | ||
209 | puzzle.updateCell2(x, y, 'line', window.LINE_NONE) | ||
210 | if (puzzle.symType != SYM_TYPE_NONE) { | ||
211 | var sym = puzzle.getSymmetricalPos(x, y) | ||
212 | puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_NONE) | ||
213 | } | ||
214 | } | ||
215 | |||
216 | // @Performance: This is the most central loop in this code. | ||
217 | // Any performance efforts should be focused here. | ||
218 | // Note: Most mechanics are NP (or harder), so don't feel bad about solving them by brute force. | ||
219 | // https://arxiv.org/pdf/1804.10193.pdf | ||
220 | function solveLoop(x, y, numEndpoints, earlyExitData) { | ||
221 | // Stop trying to solve once we reach our goal | ||
222 | if (solutionPaths.length >= window.MAX_SOLUTIONS) return | ||
223 | |||
224 | // Check for collisions (outside, gap, self, other) | ||
225 | var cell = puzzle.getCell(x, y) | ||
226 | if (cell == null) return | ||
227 | if (cell.gap > window.GAP_NONE) return | ||
228 | if (cell.line !== window.LINE_NONE) return | ||
229 | |||
230 | if (puzzle.symType == SYM_TYPE_NONE) { | ||
231 | puzzle.updateCell2(x, y, 'line', window.LINE_BLACK) | ||
232 | } else { | ||
233 | var sym = puzzle.getSymmetricalPos(x, y) | ||
234 | if (puzzle.matchesSymmetricalPos(x, y, sym.x, sym.y)) return // Would collide with our reflection | ||
235 | |||
236 | var symCell = puzzle.getCell(sym.x, sym.y) | ||
237 | if (symCell.gap > window.GAP_NONE) return | ||
238 | |||
239 | puzzle.updateCell2(x, y, 'line', window.LINE_BLUE) | ||
240 | puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_YELLOW) | ||
241 | } | ||
242 | |||
243 | if (path.length < NODE_DEPTH) nodes++ | ||
244 | |||
245 | if (cell.end != null) { | ||
246 | path.push(PATH_NONE) | ||
247 | puzzle.endPoint = {'x': x, 'y': y} | ||
248 | var puzzleData = window.validate(puzzle, true) | ||
249 | if (puzzleData.valid()) solutionPaths.push(path.slice()) | ||
250 | path.pop() | ||
251 | |||
252 | // If there are no further endpoints, tail recurse. | ||
253 | // Otherwise, keep going -- we might be able to reach another endpoint. | ||
254 | numEndpoints-- | ||
255 | if (numEndpoints === 0) return tailRecurse(x, y) | ||
256 | } | ||
257 | |||
258 | var newEarlyExitData = null | ||
259 | if (doPruning) { | ||
260 | var isEdge = x <= 0 || y <= 0 || x >= puzzle.width - 1 || y >= puzzle.height - 1 | ||
261 | newEarlyExitData = [ | ||
262 | earlyExitData[0] || (!isEdge && earlyExitData[2].isEdge), // Have we ever left an edge? | ||
263 | earlyExitData[2], // The position before our current one | ||
264 | {'x':x, 'y':y, 'isEdge':isEdge} // Our current position. | ||
265 | ] | ||
266 | if (earlyExitData[0] && !earlyExitData[1].isEdge && earlyExitData[2].isEdge && isEdge) { | ||
267 | // See the above comment for an explanation of this math. | ||
268 | var floodX = earlyExitData[2].x + (earlyExitData[1].x - x) | ||
269 | var floodY = earlyExitData[2].y + (earlyExitData[1].y - y) | ||
270 | var region = puzzle.getRegion(floodX, floodY) | ||
271 | if (region != null) { | ||
272 | var regionData = window.validateRegion(puzzle, region, true) | ||
273 | if (!regionData.valid()) return tailRecurse(x, y) | ||
274 | |||
275 | // Additionally, we might have left an endpoint in the enclosed region. | ||
276 | // If so, we should decrement the number of remaining endpoints (and possibly tail recurse). | ||
277 | for (var pos of region) { | ||
278 | var endCell = puzzle.grid[pos.x][pos.y] | ||
279 | if (endCell != null && endCell.end != null) numEndpoints-- | ||
280 | } | ||
281 | |||
282 | if (numEndpoints === 0) return tailRecurse(x, y) | ||
283 | } | ||
284 | } | ||
285 | } | ||
286 | |||
287 | if (SOLVE_SYNC || path.length > SYNC_THRESHOLD) { | ||
288 | path.push(PATH_NONE) | ||
289 | |||
290 | // Recursion order (LRUD) is optimized for BL->TR and mid-start puzzles | ||
291 | if (y%2 === 0) { | ||
292 | path[path.length-1] = PATH_LEFT | ||
293 | solveLoop(x - 1, y, numEndpoints, newEarlyExitData) | ||
294 | |||
295 | path[path.length-1] = PATH_RIGHT | ||
296 | solveLoop(x + 1, y, numEndpoints, newEarlyExitData) | ||
297 | } | ||
298 | |||
299 | if (x%2 === 0) { | ||
300 | path[path.length-1] = PATH_TOP | ||
301 | solveLoop(x, y - 1, numEndpoints, newEarlyExitData) | ||
302 | |||
303 | path[path.length-1] = PATH_BOTTOM | ||
304 | solveLoop(x, y + 1, numEndpoints, newEarlyExitData) | ||
305 | } | ||
306 | |||
307 | path.pop() | ||
308 | tailRecurse(x, y) | ||
309 | |||
310 | } else { | ||
311 | // Push a dummy element on the end of the path, so that we can fill it correctly as we DFS. | ||
312 | // This element is popped when we tail recurse (which always happens *after* all of our DFS!) | ||
313 | path.push(PATH_NONE) | ||
314 | |||
315 | // Recursion order (LRUD) is optimized for BL->TR and mid-start puzzles | ||
316 | var newTasks = [] | ||
317 | if (y%2 === 0) { | ||
318 | newTasks.push(function() { | ||
319 | path[path.length-1] = PATH_LEFT | ||
320 | return solveLoop(x - 1, y, numEndpoints, newEarlyExitData) | ||
321 | }) | ||
322 | newTasks.push(function() { | ||
323 | path[path.length-1] = PATH_RIGHT | ||
324 | return solveLoop(x + 1, y, numEndpoints, newEarlyExitData) | ||
325 | }) | ||
326 | } | ||
327 | |||
328 | if (x%2 === 0) { | ||
329 | newTasks.push(function() { | ||
330 | path[path.length-1] = PATH_TOP | ||
331 | return solveLoop(x, y - 1, numEndpoints, newEarlyExitData) | ||
332 | }) | ||
333 | newTasks.push(function() { | ||
334 | path[path.length-1] = PATH_BOTTOM | ||
335 | return solveLoop(x, y + 1, numEndpoints, newEarlyExitData) | ||
336 | }) | ||
337 | } | ||
338 | |||
339 | newTasks.push(function() { | ||
340 | path.pop() | ||
341 | tailRecurse(x, y) | ||
342 | }) | ||
343 | |||
344 | return newTasks | ||
345 | } | ||
346 | } | ||
347 | |||
348 | window.cancelSolving = function() { | ||
349 | console.info('Cancelled solving') | ||
350 | window.MAX_SOLUTIONS = 0 // Causes all new solveLoop calls to exit immediately. | ||
351 | tasks = [] | ||
352 | } | ||
353 | |||
354 | // Only modifies the puzzle object (does not do any graphics updates). Used by metapuzzle.js to determine subpuzzle polyshapes. | ||
355 | window.drawPathNoUI = function(puzzle, path) { | ||
356 | puzzle.clearLines() | ||
357 | |||
358 | // Extract the start data from the first path element | ||
359 | var x = path[0].x | ||
360 | var y = path[0].y | ||
361 | var cell = puzzle.getCell(x, y) | ||
362 | if (cell == null || cell.start !== true) throw Error('Path does not begin with a startpoint: ' + JSON.stringify(cell)) | ||
363 | |||
364 | for (var i=1; i<path.length; i++) { | ||
365 | var cell = puzzle.getCell(x, y) | ||
366 | |||
367 | var dx = 0 | ||
368 | var dy = 0 | ||
369 | if (path[i] === PATH_NONE) { // Reached an endpoint, move into it | ||
370 | console.debug('Reached endpoint') | ||
371 | if (i != path.length-1) throw Error('Path contains ' + (path.length - 1 - i) + ' trailing directions') | ||
372 | break | ||
373 | } else if (path[i] === PATH_LEFT) dx = -1 | ||
374 | else if (path[i] === PATH_RIGHT) dx = +1 | ||
375 | else if (path[i] === PATH_TOP) dy = -1 | ||
376 | else if (path[i] === PATH_BOTTOM) dy = +1 | ||
377 | else throw Error('Path element ' + (i-1) + ' was not a valid path direction: ' + path[i]) | ||
378 | |||
379 | x += dx | ||
380 | y += dy | ||
381 | // Set the cell color | ||
382 | if (puzzle.symType == SYM_TYPE_NONE) { | ||
383 | cell.line = window.LINE_BLACK | ||
384 | } else { | ||
385 | cell.line = window.LINE_BLUE | ||
386 | var sym = puzzle.getSymmetricalPos(x, y) | ||
387 | puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_YELLOW) | ||
388 | } | ||
389 | } | ||
390 | |||
391 | var cell = puzzle.getCell(x, y) | ||
392 | if (cell == null || cell.end == null) throw Error('Path does not end at an endpoint: ' + JSON.stringify(cell)) | ||
393 | } | ||
394 | |||
395 | // Uses trace2 to draw the path on the grid, logs a graphical representation of the solution, | ||
396 | // and also modifies the puzzle to contain the solution path. | ||
397 | window.drawPath = function(puzzle, path, target='puzzle') { | ||
398 | // @Duplicated with trace2.clearGrid | ||
399 | var puzzleElem = document.getElementById(target) | ||
400 | window.deleteElementsByClassName(puzzleElem, 'cursor') | ||
401 | window.deleteElementsByClassName(puzzleElem, 'line-1') | ||
402 | window.deleteElementsByClassName(puzzleElem, 'line-2') | ||
403 | window.deleteElementsByClassName(puzzleElem, 'line-3') | ||
404 | puzzle.clearLines() | ||
405 | |||
406 | if (path == null || path.length === 0) return // "drawing" an empty path is a shorthand for clearing the grid. | ||
407 | |||
408 | // Extract the start data from the first path element | ||
409 | var x = path[0].x | ||
410 | var y = path[0].y | ||
411 | var cell = puzzle.getCell(x, y) | ||
412 | if (cell == null || cell.start !== true) throw Error('Path does not begin with a startpoint: ' + JSON.stringify(cell)) | ||
413 | |||
414 | var start = document.getElementById('start_' + target + '_' + x + '_' + y) | ||
415 | var symStart = document.getElementById('symStart_' + target + '_' + x + '_' + y) | ||
416 | window.onTraceStart(puzzle, {'x':x, 'y':y}, document.getElementById(target), start, symStart) | ||
417 | |||
418 | console.info('Drawing solution of length', path.length) | ||
419 | for (var i=1; i<path.length; i++) { | ||
420 | var cell = puzzle.getCell(x, y) | ||
421 | |||
422 | var dx = 0 | ||
423 | var dy = 0 | ||
424 | if (path[i] === PATH_NONE) { // Reached an endpoint, move into it | ||
425 | console.debug('Reached endpoint') | ||
426 | if (cell.end === 'left') { | ||
427 | window.onMove(-24, 0) | ||
428 | } else if (cell.end === 'right') { | ||
429 | window.onMove(24, 0) | ||
430 | } else if (cell.end === 'top') { | ||
431 | window.onMove(0, -24) | ||
432 | } else if (cell.end === 'bottom') { | ||
433 | window.onMove(0, 24) | ||
434 | } | ||
435 | if (i != path.length-1) throw Error('Path contains ' + (path.length - 1 - i) + ' trailing directions') | ||
436 | break | ||
437 | } else if (path[i] === PATH_LEFT) { | ||
438 | dx = -1 | ||
439 | cell.dir = 'left' | ||
440 | } else if (path[i] === PATH_RIGHT) { | ||
441 | dx = +1 | ||
442 | cell.dir = 'right' | ||
443 | } else if (path[i] === PATH_TOP) { | ||
444 | dy = -1 | ||
445 | cell.dir = 'top' | ||
446 | } else if (path[i] === PATH_BOTTOM) { | ||
447 | dy = +1 | ||
448 | cell.dir = 'down' | ||
449 | } else { | ||
450 | throw Error('Path element ' + (i-1) + ' was not a valid path direction: ' + path[i]) | ||
451 | } | ||
452 | |||
453 | console.debug('Currently at', x, y, cell, 'moving', dx, dy) | ||
454 | |||
455 | x += dx | ||
456 | y += dy | ||
457 | // Unflag the cell, move into it, and reflag it | ||
458 | cell.line = window.LINE_NONE | ||
459 | window.onMove(41 * dx, 41 * dy) | ||
460 | if (puzzle.symType == SYM_TYPE_NONE) { | ||
461 | cell.line = window.LINE_BLACK | ||
462 | } else { | ||
463 | cell.line = window.LINE_BLUE | ||
464 | var sym = puzzle.getSymmetricalPos(x, y) | ||
465 | puzzle.updateCell2(sym.x, sym.y, 'line', window.LINE_YELLOW) | ||
466 | } | ||
467 | } | ||
468 | var cell = puzzle.getCell(x, y) | ||
469 | if (cell == null || cell.end == null) throw Error('Path does not end at an endpoint: ' + JSON.stringify(cell)) | ||
470 | |||
471 | var rows = ' |' | ||
472 | for (var x=0; x<puzzle.width; x++) { | ||
473 | rows += ('' + x).padEnd(5, ' ') + '|' | ||
474 | } | ||
475 | console.log(rows) | ||
476 | for (var y=0; y<puzzle.height; y++) { | ||
477 | var output = ('' + y).padEnd(3, ' ') + '|' | ||
478 | for (var x=0; x<puzzle.width; x++) { | ||
479 | var cell = puzzle.grid[x][y] | ||
480 | var dir = (cell != null && cell.dir != null ? cell.dir : '') | ||
481 | output += dir.padEnd(5, ' ') + '|' | ||
482 | } | ||
483 | console.log(output) | ||
484 | } | ||
485 | } | ||
486 | |||
487 | window.getSolutionIndex = function(pathList, solution) { | ||
488 | for (var i=0; i<pathList.length; i++) { | ||
489 | var path = pathList[i] | ||
490 | var x = path[0].x | ||
491 | var y = path[0].y | ||
492 | if (solution.grid[path[0].x][path[0].y].line === 0) continue | ||
493 | |||
494 | var match = true | ||
495 | for (var j=1; j<path.length; j++) { | ||
496 | var cell = solution.grid[x][y] | ||
497 | if (path[j] === PATH_NONE && cell.end != null) { | ||
498 | match = false | ||
499 | break | ||
500 | } else if (path[j] === PATH_LEFT) { | ||
501 | if (cell.dir != 'left') { | ||
502 | match = false | ||
503 | break | ||
504 | } | ||
505 | x-- | ||
506 | } else if (path[j] === PATH_RIGHT) { | ||
507 | if (cell.dir != 'right') { | ||
508 | match = false | ||
509 | break | ||
510 | } | ||
511 | x++ | ||
512 | } else if (path[j] === PATH_TOP) { | ||
513 | if (cell.dir != 'top') { | ||
514 | match = false | ||
515 | break | ||
516 | } | ||
517 | y-- | ||
518 | } else if (path[j] === PATH_BOTTOM) { | ||
519 | if (cell.dir != 'bottom') { | ||
520 | match = false | ||
521 | break | ||
522 | } | ||
523 | y++ | ||
524 | } | ||
525 | } | ||
526 | if (match) return i | ||
527 | } | ||
528 | return -1 | ||
529 | } | ||
530 | |||
531 | }) | ||