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 | }) | ||