diff options
| author | Star Rauchenberger <fefferburbia@gmail.com> | 2023-11-30 13:29:08 -0500 |
|---|---|---|
| committer | Star Rauchenberger <fefferburbia@gmail.com> | 2023-11-30 13:29:08 -0500 |
| commit | 0929719a845897cc8567cf972e07a69a71f0fa6f (patch) | |
| tree | 2b6f69c1d906abb6e0abf8a0f1d51725bc78087d /app/assets/javascripts/polyominos.js | |
| parent | 01c1947537e4e23ded0c16812a7cd9d49ad88356 (diff) | |
| download | wittle-0929719a845897cc8567cf972e07a69a71f0fa6f.tar.gz wittle-0929719a845897cc8567cf972e07a69a71f0fa6f.tar.bz2 wittle-0929719a845897cc8567cf972e07a69a71f0fa6f.zip | |
Migrate to a full rails app
Diffstat (limited to 'app/assets/javascripts/polyominos.js')
| -rw-r--r-- | app/assets/javascripts/polyominos.js | 331 |
1 files changed, 331 insertions, 0 deletions
| diff --git a/app/assets/javascripts/polyominos.js b/app/assets/javascripts/polyominos.js new file mode 100644 index 0000000..4d8be6e --- /dev/null +++ b/app/assets/javascripts/polyominos.js | |||
| @@ -0,0 +1,331 @@ | |||
| 1 | namespace(function() { | ||
| 2 | |||
| 3 | function getPolySize(polyshape) { | ||
| 4 | var size = 0 | ||
| 5 | for (var x=0; x<4; x++) { | ||
| 6 | for (var y=0; y<4; y++) { | ||
| 7 | if (isSet(polyshape, x, y)) size++ | ||
| 8 | } | ||
| 9 | } | ||
| 10 | return size | ||
| 11 | } | ||
| 12 | |||
| 13 | function mask(x, y) { | ||
| 14 | return 1 << ((3-y)*4 + x) | ||
| 15 | } | ||
| 16 | |||
| 17 | function isSet(polyshape, x, y) { | ||
| 18 | if (x < 0 || y < 0) return false | ||
| 19 | if (x >= 4 || y >= 4) return false | ||
| 20 | return (polyshape & mask(x, y)) !== 0 | ||
| 21 | } | ||
| 22 | |||
| 23 | // This is 2^20, whereas all the other bits fall into 2^(0-15) | ||
| 24 | window.ROTATION_BIT = (1 << 20) | ||
| 25 | |||
| 26 | window.isRotated = function(polyshape) { | ||
| 27 | return (polyshape & ROTATION_BIT) !== 0 | ||
| 28 | } | ||
| 29 | |||
| 30 | function getRotations(polyshape) { | ||
| 31 | if (!isRotated(polyshape)) return [polyshape] | ||
| 32 | |||
| 33 | var rotations = [0, 0, 0, 0] | ||
| 34 | for (var x=0; x<4; x++) { | ||
| 35 | for (var y=0; y<4; y++) { | ||
| 36 | if (isSet(polyshape, x, y)) { | ||
| 37 | rotations[0] ^= mask(x, y) | ||
| 38 | rotations[1] ^= mask(y, 3-x) | ||
| 39 | rotations[2] ^= mask(3-x, 3-y) | ||
| 40 | rotations[3] ^= mask(3-y, x) | ||
| 41 | } | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | return rotations | ||
| 46 | } | ||
| 47 | |||
| 48 | // 90 degree rotations of the polyomino | ||
| 49 | window.rotatePolyshape = function(polyshape, count=1) { | ||
| 50 | var rotations = getRotations(polyshape | window.ROTATION_BIT) | ||
| 51 | return rotations[count % 4] | ||
| 52 | } | ||
| 53 | |||
| 54 | // IMPORTANT NOTE: When formulating these, the top row must contain (0, 0) | ||
| 55 | // That means there will never be any negative y values. | ||
| 56 | // (0, 0) must also be a cell in the shape, so that | ||
| 57 | // placing the shape at (x, y) will fill (x, y) | ||
| 58 | // Ylops will have -1s on all adjacent cells, to break "overlaps" for polyominos. | ||
| 59 | window.polyominoFromPolyshape = function(polyshape, ylop=false, precise=true) { | ||
| 60 | var topLeft = null | ||
| 61 | for (var y=0; y<4; y++) { | ||
| 62 | for (var x=0; x<4; x++) { | ||
| 63 | if (isSet(polyshape, x, y)) { | ||
| 64 | topLeft = {'x':x, 'y':y} | ||
| 65 | break | ||
| 66 | } | ||
| 67 | } | ||
| 68 | if (topLeft != null) break | ||
| 69 | } | ||
| 70 | if (topLeft == null) return [] // Empty polyomino | ||
| 71 | |||
| 72 | var polyomino = [] | ||
| 73 | for (var x=0; x<4; x++) { | ||
| 74 | for (var y=0; y<4; y++) { | ||
| 75 | if (!isSet(polyshape, x, y)) continue | ||
| 76 | polyomino.push({'x':2*(x - topLeft.x), 'y':2*(y - topLeft.y)}) | ||
| 77 | |||
| 78 | // "Precise" polyominos adds cells in between the apparent squares in the polyomino. | ||
| 79 | // This prevents the solution line from going through polyominos in the solution. | ||
| 80 | if (precise) { | ||
| 81 | if (ylop) { | ||
| 82 | // Ylops fill up/left if no adjacent cell, and always fill bottom/right | ||
| 83 | if (!isSet(polyshape, x - 1, y)) { | ||
| 84 | polyomino.push({'x':2*(x - topLeft.x) - 1, 'y':2*(y - topLeft.y)}) | ||
| 85 | } | ||
| 86 | if (!isSet(polyshape, x, y - 1)) { | ||
| 87 | polyomino.push({'x':2*(x - topLeft.x), 'y':2*(y - topLeft.y) - 1}) | ||
| 88 | } | ||
| 89 | polyomino.push({'x':2*(x - topLeft.x) + 1, 'y':2*(y - topLeft.y)}) | ||
| 90 | polyomino.push({'x':2*(x - topLeft.x), 'y':2*(y - topLeft.y) + 1}) | ||
| 91 | } else { | ||
| 92 | // Normal polys only fill bottom/right if there is an adjacent cell. | ||
| 93 | if (isSet(polyshape, x + 1, y)) { | ||
| 94 | polyomino.push({'x':2*(x - topLeft.x) + 1, 'y':2*(y - topLeft.y)}) | ||
| 95 | } | ||
| 96 | if (isSet(polyshape, x, y + 1)) { | ||
| 97 | polyomino.push({'x':2*(x - topLeft.x), 'y':2*(y - topLeft.y) + 1}) | ||
| 98 | } | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | } | ||
| 103 | return polyomino | ||
| 104 | } | ||
| 105 | |||
| 106 | window.polyshapeFromPolyomino = function(polyomino) { | ||
| 107 | var topLeft = {'x': 9999, 'y': 9999} | ||
| 108 | for (var pos of polyomino) { | ||
| 109 | if (pos.x%2 != 1 || pos.y%2 != 1) continue // We only care about cells, not edges or intersections | ||
| 110 | |||
| 111 | // Unlike when we're making a polyomino, we just want to top and left flush the shape, | ||
| 112 | // we don't actually need (0, 0) to be filled. | ||
| 113 | if (pos.x < topLeft.x) topLeft.x = pos.x | ||
| 114 | if (pos.y < topLeft.y) topLeft.y = pos.y | ||
| 115 | } | ||
| 116 | if (topLeft == null) return 0 // Empty polyomino | ||
| 117 | |||
| 118 | var polyshape = 0 | ||
| 119 | for (var pos of polyomino) { | ||
| 120 | if (pos.x%2 != 1 || pos.y%2 != 1) continue // We only care about cells, not edges or intersections | ||
| 121 | var x = (pos.x - topLeft.x) / 2 // 0.5x to convert from puzzle coordinates to polyshape coordinates | ||
| 122 | var y = (pos.y - topLeft.y) / 2 // 0.5x to convert from puzzle coordinates to polyshape coordinates | ||
| 123 | polyshape |= mask(x, y) | ||
| 124 | } | ||
| 125 | |||
| 126 | return polyshape | ||
| 127 | } | ||
| 128 | |||
| 129 | // In some cases, polyominos and onimoylops will fully cancel each other out. | ||
| 130 | // However, even if they are the same size, that doesn't guarantee that they fit together. | ||
| 131 | // As an optimization, we save the results for known combinations of shapes, since there are likely many | ||
| 132 | // fewer pairings of shapes than paths through the grid. | ||
| 133 | var knownCancellations = {} | ||
| 134 | |||
| 135 | // Attempt to fit polyominos in a region into the puzzle. | ||
| 136 | // This function checks for early exits, then simplifies the grid to a numerical representation: | ||
| 137 | // * 1 represents a square that has been double-covered (by two polyominos) | ||
| 138 | // * Or, in the cancellation case, it represents a square that was covered by a polyomino and not by an onimoylop | ||
| 139 | // * 0 represents a square that is satisfied, either because: | ||
| 140 | // * it is outside the region | ||
| 141 | // * (In the normal case) it was inside the region, and has been covered by a polyomino | ||
| 142 | // * (In the cancellation case) it was covered by an equal number of polyominos and onimoylops | ||
| 143 | // * -1 represents a square that needs to be covered once (inside the region, or outside but covered by an onimoylop) | ||
| 144 | // * -2 represents a square that needs to be covered twice (inside the region & covered by an onimoylop) | ||
| 145 | // * And etc, for additional layers of polyominos/onimoylops. | ||
| 146 | window.polyFit = function(region, puzzle) { | ||
| 147 | var polys = [] | ||
| 148 | var ylops = [] | ||
| 149 | var polyCount = 0 | ||
| 150 | var regionSize = 0 | ||
| 151 | for (var pos of region) { | ||
| 152 | if (pos.x%2 === 1 && pos.y%2 === 1) regionSize++ | ||
| 153 | var cell = puzzle.grid[pos.x][pos.y] | ||
| 154 | if (cell == null) continue | ||
| 155 | if (cell.polyshape === 0) continue | ||
| 156 | if (cell.type === 'poly') { | ||
| 157 | polys.push(cell) | ||
| 158 | polyCount += getPolySize(cell.polyshape) | ||
| 159 | } else if (cell.type === 'ylop') { | ||
| 160 | ylops.push(cell) | ||
| 161 | polyCount -= getPolySize(cell.polyshape) | ||
| 162 | } | ||
| 163 | } | ||
| 164 | if (polys.length + ylops.length === 0) { | ||
| 165 | console.log('No polyominos or onimoylops inside the region, vacuously true') | ||
| 166 | return true | ||
| 167 | } | ||
| 168 | if (polyCount > 0 && polyCount !== regionSize) { | ||
| 169 | console.log('Combined size of polyominos and onimoylops', polyCount, 'does not match region size', regionSize) | ||
| 170 | return false | ||
| 171 | } | ||
| 172 | if (polyCount < 0) { | ||
| 173 | console.log('Combined size of onimoylops is greater than polyominos by', -polyCount) | ||
| 174 | return false | ||
| 175 | } | ||
| 176 | var key = null | ||
| 177 | if (polyCount === 0) { | ||
| 178 | if (puzzle.settings.SHAPELESS_ZERO_POLY) { | ||
| 179 | console.log('Combined size of polyominos and onimoylops is zero') | ||
| 180 | return true | ||
| 181 | } | ||
| 182 | // These will be ordered by the order of cells in the region, which isn't exactly consistent. | ||
| 183 | // In practice, it seems to be good enough. | ||
| 184 | key = '' | ||
| 185 | for (var ylop of ylops) key += ' ' + ylop.polyshape | ||
| 186 | key += '|' | ||
| 187 | for (var poly of polys) key += ' ' + poly.polyshape | ||
| 188 | var ret = knownCancellations[key] | ||
| 189 | if (ret != null) return ret | ||
| 190 | } | ||
| 191 | |||
| 192 | // For polyominos, we clear the grid to mark it up again: | ||
| 193 | var savedGrid = puzzle.grid | ||
| 194 | puzzle.newGrid() | ||
| 195 | // First, we mark all cells as 0: Cells outside the target region should be unaffected. | ||
| 196 | for (var x=0; x<puzzle.width; x++) { | ||
| 197 | for (var y=0; y<puzzle.height; y++) { | ||
| 198 | puzzle.setCell(x, y, 0) | ||
| 199 | } | ||
| 200 | } | ||
| 201 | // In the normal case, we mark every cell as -1: It needs to be covered by one poly | ||
| 202 | if (polyCount > 0) { | ||
| 203 | for (var pos of region) puzzle.grid[pos.x][pos.y] = -1 | ||
| 204 | } | ||
| 205 | // In the exact match case, we leave every cell marked 0: Polys and ylops need to cancel. | ||
| 206 | |||
| 207 | var ret = placeYlops(ylops, 0, polys, puzzle) | ||
| 208 | if (polyCount === 0) knownCancellations[key] = ret | ||
| 209 | puzzle.grid = savedGrid | ||
| 210 | return ret | ||
| 211 | } | ||
| 212 | |||
| 213 | // If false, poly doesn't fit and grid is unmodified | ||
| 214 | // If true, poly fits and grid is modified (with the placement) | ||
| 215 | function tryPlacePolyshape(cells, x, y, puzzle, sign) { | ||
| 216 | console.spam('Placing at', x, y, 'with sign', sign) | ||
| 217 | var numCells = cells.length | ||
| 218 | for (var i=0; i<numCells; i++) { | ||
| 219 | var cell = cells[i] | ||
| 220 | var puzzleCell = puzzle.getCell(cell.x + x, cell.y + y) | ||
| 221 | if (puzzleCell == null) return false | ||
| 222 | cell.value = puzzleCell | ||
| 223 | } | ||
| 224 | for (var i=0; i<numCells; i++) { | ||
| 225 | var cell = cells[i] | ||
| 226 | puzzle.setCell(cell.x + x, cell.y + y, cell.value + sign) | ||
| 227 | } | ||
| 228 | return true | ||
| 229 | } | ||
| 230 | |||
| 231 | // Places the ylops such that they are inside of the grid, then checks if the polys | ||
| 232 | // zero the region. | ||
| 233 | function placeYlops(ylops, i, polys, puzzle) { | ||
| 234 | // Base case: No more ylops to place, start placing polys | ||
| 235 | if (i === ylops.length) return placePolys(polys, puzzle) | ||
| 236 | |||
| 237 | var ylop = ylops[i] | ||
| 238 | var ylopRotations = getRotations(ylop.polyshape) | ||
| 239 | for (var x=1; x<puzzle.width; x+=2) { | ||
| 240 | for (var y=1; y<puzzle.height; y+=2) { | ||
| 241 | console.log('Placing ylop', ylop, 'at', x, y) | ||
| 242 | for (var polyshape of ylopRotations) { | ||
| 243 | var cells = polyominoFromPolyshape(polyshape, true, puzzle.settings.PRECISE_POLYOMINOS) | ||
| 244 | if (!tryPlacePolyshape(cells, x, y, puzzle, -1)) continue | ||
| 245 | console.group('') | ||
| 246 | if (placeYlops(ylops, i+1, polys, puzzle)) return true | ||
| 247 | console.groupEnd('') | ||
| 248 | if (!tryPlacePolyshape(cells, x, y, puzzle, +1)) continue | ||
| 249 | } | ||
| 250 | } | ||
| 251 | } | ||
| 252 | console.log('Tried all ylop placements with no success.') | ||
| 253 | return false | ||
| 254 | } | ||
| 255 | |||
| 256 | // Returns whether or not a set of polyominos fit into a region. | ||
| 257 | // Solves via recursive backtracking: Some piece must fill the top left square, | ||
| 258 | // so try every piece to fill it, then recurse. | ||
| 259 | function placePolys(polys, puzzle) { | ||
| 260 | // Check for overlapping polyominos, and handle exit cases for all polyominos placed. | ||
| 261 | var allPolysPlaced = (polys.length === 0) | ||
| 262 | for (var x=0; x<puzzle.width; x++) { | ||
| 263 | var row = puzzle.grid[x] | ||
| 264 | for (var y=0; y<puzzle.height; y++) { | ||
| 265 | var cell = row[y] | ||
| 266 | if (cell > 0) { | ||
| 267 | console.log('Cell', x, y, 'has been overfilled and no ylops left to place') | ||
| 268 | return false | ||
| 269 | } | ||
| 270 | if (allPolysPlaced && cell < 0 && x%2 === 1 && y%2 === 1) { | ||
| 271 | // Normal, center cell with a negative value & no polys remaining. | ||
| 272 | console.log('All polys placed, but grid not full') | ||
| 273 | return false | ||
| 274 | } | ||
| 275 | } | ||
| 276 | } | ||
| 277 | if (allPolysPlaced) { | ||
| 278 | console.log('All polys placed, and grid full') | ||
| 279 | return true | ||
| 280 | } | ||
| 281 | |||
| 282 | // The top-left (first open cell) must be filled by a polyomino. | ||
| 283 | // However in the case of pillars, there is no top-left, so we try all open cells in the | ||
| 284 | // top-most open row | ||
| 285 | var openCells = [] | ||
| 286 | for (var y=1; y<puzzle.height; y+=2) { | ||
| 287 | for (var x=1; x<puzzle.width; x+=2) { | ||
| 288 | if (puzzle.grid[x][y] >= 0) continue | ||
| 289 | openCells.push({'x':x, 'y':y}) | ||
| 290 | if (puzzle.pillar === false) break | ||
| 291 | } | ||
| 292 | if (openCells.length > 0) break | ||
| 293 | } | ||
| 294 | |||
| 295 | if (openCells.length === 0) { | ||
| 296 | console.log('Polys remaining but grid full') | ||
| 297 | return false | ||
| 298 | } | ||
| 299 | |||
| 300 | for (var openCell of openCells) { | ||
| 301 | var attemptedPolyshapes = [] | ||
| 302 | for (var i=0; i<polys.length; i++) { | ||
| 303 | var poly = polys[i] | ||
| 304 | console.spam('Selected poly', poly) | ||
| 305 | if (attemptedPolyshapes.includes(poly.polyshape)) { | ||
| 306 | console.spam('Polyshape', poly.polyshape, 'has already been attempted') | ||
| 307 | continue | ||
| 308 | } | ||
| 309 | attemptedPolyshapes.push(poly.polyshape) | ||
| 310 | polys.splice(i, 1) | ||
| 311 | for (var polyshape of getRotations(poly.polyshape)) { | ||
| 312 | console.spam('Selected polyshape', polyshape) | ||
| 313 | var cells = polyominoFromPolyshape(polyshape, false, puzzle.settings.PRECISE_POLYOMINOS) | ||
| 314 | if (!tryPlacePolyshape(cells, openCell.x, openCell.y, puzzle, +1)) { | ||
| 315 | console.spam('Polyshape', polyshape, 'does not fit into', openCell.x, openCell.y) | ||
| 316 | continue | ||
| 317 | } | ||
| 318 | console.group('') | ||
| 319 | if (placePolys(polys, puzzle)) return true | ||
| 320 | console.groupEnd('') | ||
| 321 | // Should not fail, as it's an inversion of the above tryPlacePolyshape | ||
| 322 | tryPlacePolyshape(cells, openCell.x, openCell.y, puzzle, -1) | ||
| 323 | } | ||
| 324 | polys.splice(i, 0, poly) | ||
| 325 | } | ||
| 326 | } | ||
| 327 | console.log('Grid non-empty with >0 polys, but no valid recursion.') | ||
| 328 | return false | ||
| 329 | } | ||
| 330 | |||
| 331 | }) | ||
