about summary refs log tree commit diff stats
path: root/app/assets/javascripts/polyominos.js
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2023-11-30 13:29:08 -0500
committerStar Rauchenberger <fefferburbia@gmail.com>2023-11-30 13:29:08 -0500
commit0929719a845897cc8567cf972e07a69a71f0fa6f (patch)
tree2b6f69c1d906abb6e0abf8a0f1d51725bc78087d /app/assets/javascripts/polyominos.js
parent01c1947537e4e23ded0c16812a7cd9d49ad88356 (diff)
downloadwittle-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.js331
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 @@
1namespace(function() {
2
3function 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
13function mask(x, y) {
14 return 1 << ((3-y)*4 + x)
15}
16
17function 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)
24window.ROTATION_BIT = (1 << 20)
25
26window.isRotated = function(polyshape) {
27 return (polyshape & ROTATION_BIT) !== 0
28}
29
30function 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
49window.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.
59window.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
106window.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.
133var 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.
146window.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)
215function 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.
233function 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.
259function 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})