diff options
Diffstat (limited to 'app/assets/javascripts/serializer.js')
-rw-r--r-- | app/assets/javascripts/serializer.js | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/app/assets/javascripts/serializer.js b/app/assets/javascripts/serializer.js new file mode 100644 index 0000000..70c7f0f --- /dev/null +++ b/app/assets/javascripts/serializer.js | |||
@@ -0,0 +1,365 @@ | |||
1 | namespace(function() { | ||
2 | |||
3 | window.serializePuzzle = function(puzzle) { | ||
4 | var s = new Serializer('w') | ||
5 | var version = 0 | ||
6 | |||
7 | s.writeInt(version) | ||
8 | s.writeByte(puzzle.width) | ||
9 | s.writeByte(puzzle.height) | ||
10 | s.writeString(puzzle.name) | ||
11 | |||
12 | var genericFlags = 0 | ||
13 | if (puzzle.autoSolved) genericFlags |= GENERIC_FLAG_AUTOSOLVED | ||
14 | if (puzzle.symmetry) { | ||
15 | genericFlags |= GENERIC_FLAG_SYMMETRICAL | ||
16 | if (puzzle.symmetry.x) genericFlags |= GENERIC_FLAG_SYMMETRY_X | ||
17 | if (puzzle.symmetry.y) genericFlags |= GENERIC_FLAG_SYMMETRY_Y | ||
18 | } | ||
19 | if (puzzle.pillar) genericFlags |= GENERIC_FLAG_PILLAR | ||
20 | s.writeByte(genericFlags) | ||
21 | for (var x=0; x<puzzle.width; x++) { | ||
22 | for (var y=0; y<puzzle.height; y++) { | ||
23 | s.writeCell(puzzle.grid[x][y]) | ||
24 | } | ||
25 | } | ||
26 | |||
27 | if (puzzle.path != null) { | ||
28 | var startPos = puzzle.path.pop() | ||
29 | if (puzzle.path.length > 0) { | ||
30 | s.writeInt(puzzle.path.length) | ||
31 | s.writeByte(startPos.x) | ||
32 | s.writeByte(startPos.y) | ||
33 | for (var dir of puzzle.path) s.writeByte(dir) | ||
34 | } | ||
35 | } else { | ||
36 | s.writeInt(0) | ||
37 | } | ||
38 | |||
39 | var settingsFlags = 0 | ||
40 | if (puzzle.settings.NEGATIONS_CANCEL_NEGATIONS) settingsFlags |= SETTINGS_FLAG_NCN | ||
41 | if (puzzle.settings.SHAPELESS_ZERO_POLY) settingsFlags |= SETTINGS_FLAG_SZP | ||
42 | if (puzzle.settings.PRECISE_POLYOMINOS) settingsFlags |= SETTINGS_FLAG_PP | ||
43 | if (puzzle.settings.FLASH_FOR_ERRORS) settingsFlags |= SETTINGS_FLAG_FFE | ||
44 | if (puzzle.settings.FAT_STARTPOINTS) settingsFlags |= SETTINGS_FLAG_FS | ||
45 | if (puzzle.settings.CUSTOM_MECHANICS) settingsFlags |= SETTINGS_FLAG_CM | ||
46 | if (puzzle.settings.INVISIBLE_SYMMETRY) settingsFlags |= SETTINGS_FLAG_IS | ||
47 | s.writeByte(settingsFlags) | ||
48 | |||
49 | s.writeByte(puzzle.symType) | ||
50 | |||
51 | return s.str() | ||
52 | } | ||
53 | |||
54 | window.deserializePuzzle = function(data) { | ||
55 | // Data is JSON, so decode it with the old deserializer | ||
56 | if (data[0] == '{') return Puzzle.deserialize(data) | ||
57 | |||
58 | var s = new Serializer('r', data) | ||
59 | var version = s.readInt() | ||
60 | if (version > 0) throw Error('Cannot read data from unknown version: ' + version) | ||
61 | |||
62 | var width = s.readByte() | ||
63 | var height = s.readByte() | ||
64 | var puzzle = new Puzzle(Math.floor(width / 2), Math.floor(height / 2)) | ||
65 | puzzle.name = s.readString() | ||
66 | |||
67 | var genericFlags = s.readByte() | ||
68 | puzzle.autoSolved = genericFlags & GENERIC_FLAG_AUTOSOLVED | ||
69 | puzzle.symType = SYM_TYPE_NONE | ||
70 | if ((genericFlags & GENERIC_FLAG_SYMMETRICAL) != 0) { | ||
71 | if ((genericFlags & GENERIC_FLAG_SYMMETRY_X) != 0) { | ||
72 | if ((genericFlags & GENERIC_FLAG_SYMMETRY_Y) != 0) { | ||
73 | puzzle.symType = SYM_TYPE_ROTATIONAL | ||
74 | } else { | ||
75 | puzzle.symType = SYM_TYPE_VERTICAL | ||
76 | } | ||
77 | } else if ((genericFlags & GENERIC_FLAG_SYMMETRY_Y) != 0) { | ||
78 | puzzle.symType = SYM_TYPE_HORIZONTAL | ||
79 | } | ||
80 | } | ||
81 | puzzle.pillar = (genericFlags & GENERIC_FLAG_PILLAR) != 0 | ||
82 | for (var x=0; x<width; x++) { | ||
83 | for (var y=0; y<height; y++) { | ||
84 | puzzle.grid[x][y] = s.readCell() | ||
85 | } | ||
86 | } | ||
87 | |||
88 | var pathLength = s.readInt() | ||
89 | if (pathLength > 0) { | ||
90 | var path = [{ | ||
91 | 'x': s.readByte(), | ||
92 | 'y': s.readByte(), | ||
93 | }] | ||
94 | for (var i=0; i<pathLength; i++) path.push(s.readByte()) | ||
95 | } | ||
96 | |||
97 | var settingsFlags = s.readByte() | ||
98 | puzzle.settings = { | ||
99 | NEGATIONS_CANCEL_NEGATIONS: (settingsFlags & SETTINGS_FLAG_NCN) != 0, | ||
100 | SHAPELESS_ZERO_POLY: (settingsFlags & SETTINGS_FLAG_SZP) != 0, | ||
101 | PRECISE_POLYOMINOS: (settingsFlags & SETTINGS_FLAG_PP) != 0, | ||
102 | FLASH_FOR_ERRORS: (settingsFlags & SETTINGS_FLAG_FFE) != 0, | ||
103 | FAT_STARTPOINTS: (settingsFlags & SETTINGS_FLAG_FS) != 0, | ||
104 | CUSTOM_MECHANICS: (settingsFlags & SETTINGS_FLAG_CM) != 0, | ||
105 | INVISIBLE_SYMMETRY: (settingsFlags & SETTINGS_FLAG_IS) != 0, | ||
106 | } | ||
107 | |||
108 | if (s.hasLeft(1)) { | ||
109 | puzzle.symType = s.readByte() | ||
110 | } | ||
111 | |||
112 | s.destroy() | ||
113 | return puzzle | ||
114 | } | ||
115 | |||
116 | class Serializer { | ||
117 | constructor(mode, data) { | ||
118 | this.mode = mode | ||
119 | if (mode == 'r') { | ||
120 | if (data == null) throw Error('No data provided to a read constructor') | ||
121 | if (data[0] != '_') throw Error('Cannot read data, improperly prefixed') | ||
122 | this.data = window.atob(data.slice(1)) | ||
123 | this.index = 0 | ||
124 | } else if (mode == 'w') { | ||
125 | this.data = [] | ||
126 | |||
127 | var canvas = document.createElement('canvas') | ||
128 | canvas.height = 1 | ||
129 | canvas.width = 1 | ||
130 | this.colorConverter = canvas.getContext('2d') | ||
131 | } else { | ||
132 | throw Error('Unknown serializer mode: ' + mode) | ||
133 | } | ||
134 | |||
135 | } | ||
136 | |||
137 | destroy() { | ||
138 | if (this.mode == 'r' && this.index < this.data.length) { | ||
139 | throw Error('Read not done, ' + (this.data.length - this.index) + ' bytes remain') | ||
140 | } | ||
141 | } | ||
142 | |||
143 | str() { | ||
144 | if (this.mode != 'w') throw Error('Cannot get string from a serializer opened in mode: ' + this.mode) | ||
145 | return '_' + window.btoa(this.data) | ||
146 | } | ||
147 | |||
148 | _checkRead(numBytes = 1) { | ||
149 | if (this.mode != 'r') throw Error('Cannot read data from a serializer opened in mode: ' + this.mode) | ||
150 | if (this.data.length < numBytes) throw Error('Cannot read ' + numBytes + ' bytes from a stream with only '+ this.data.length + ' bytes') | ||
151 | } | ||
152 | |||
153 | hasLeft(numBytes = 1) { | ||
154 | return ((this.data.length - this.index) >= numBytes) | ||
155 | } | ||
156 | |||
157 | readByte() { | ||
158 | this._checkRead() | ||
159 | return this.data.charCodeAt(this.index++) | ||
160 | } | ||
161 | |||
162 | writeByte(b) { | ||
163 | if (b < 0 || b > 0xFF) throw Error('Cannot write out-of-range byte ' + b) | ||
164 | this.data += String.fromCharCode(b) | ||
165 | } | ||
166 | |||
167 | readInt() { | ||
168 | var b1 = this.readByte() << 0 | ||
169 | var b2 = this.readByte() << 8 | ||
170 | var b3 = this.readByte() << 16 | ||
171 | var b4 = this.readByte() << 24 | ||
172 | return b1 | b2 | b3 | b4 | ||
173 | } | ||
174 | |||
175 | writeInt(i) { | ||
176 | if (i < 0 || i > 0xFFFFFFFF) throw Error('Cannot write out-of-range int ' + i) | ||
177 | var b1 = (i & 0x000000FF) >> 0 | ||
178 | var b2 = (i & 0x0000FF00) >> 8 | ||
179 | var b3 = (i & 0x00FF0000) >> 16 | ||
180 | var b4 = (i & 0xFF000000) >> 24 | ||
181 | this.writeByte(b1) | ||
182 | this.writeByte(b2) | ||
183 | this.writeByte(b3) | ||
184 | this.writeByte(b4) | ||
185 | } | ||
186 | |||
187 | readLong() { | ||
188 | var i1 = this.readInt() << 32 | ||
189 | var i2 = this.readInt() | ||
190 | return i1 | i2 | ||
191 | } | ||
192 | |||
193 | writeLong(l) { | ||
194 | if (l < 0 || l > 0xFFFFFFFFFFFFFFFF) throw Error('Cannot write out-of-range long ' + l) | ||
195 | var i1 = l & 0xFFFFFFFF | ||
196 | var i2 = (l - i1) / 0x100000000 | ||
197 | this.writeInt(i1) | ||
198 | this.writeInt(i2) | ||
199 | } | ||
200 | |||
201 | readString() { | ||
202 | var len = this.readInt() | ||
203 | this._checkRead(len) | ||
204 | var str = this.data.substr(this.index, len) | ||
205 | this.index += len | ||
206 | return str | ||
207 | } | ||
208 | |||
209 | writeString(s) { | ||
210 | if (s == null) { | ||
211 | this.writeInt(0) | ||
212 | return | ||
213 | } | ||
214 | this.writeInt(s.length) | ||
215 | this.data += s | ||
216 | } | ||
217 | |||
218 | readColor() { | ||
219 | var r = this.readByte().toString() | ||
220 | var g = this.readByte().toString() | ||
221 | var b = this.readByte().toString() | ||
222 | var a = this.readByte().toString() | ||
223 | return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')' | ||
224 | } | ||
225 | |||
226 | writeColor(c) { | ||
227 | // Adapted from https://gist.github.com/njvack/02ad8efcb0d552b0230d | ||
228 | this.colorConverter.fillStyle = 'rgba(0, 0, 0, 0)' // Load a default in case we are passed garbage | ||
229 | this.colorConverter.clearRect(0, 0, 1, 1) | ||
230 | this.colorConverter.fillStyle = c | ||
231 | this.colorConverter.fillRect(0, 0, 1, 1) | ||
232 | var rgba = this.colorConverter.getImageData(0, 0, 1, 1).data | ||
233 | this.writeByte(rgba[0]) | ||
234 | this.writeByte(rgba[1]) | ||
235 | this.writeByte(rgba[2]) | ||
236 | this.writeByte(rgba[3]) | ||
237 | } | ||
238 | |||
239 | readCell() { | ||
240 | var cellType = this.readByte() | ||
241 | if (cellType === CELL_TYPE_NULL) return null | ||
242 | |||
243 | var cell = {} | ||
244 | cell.dir = null | ||
245 | cell.line = 0 | ||
246 | if (cellType === CELL_TYPE_LINE) { | ||
247 | cell.type = 'line' | ||
248 | cell.line = this.readByte() | ||
249 | var dot = this.readByte() | ||
250 | if (dot != 0) cell.dot = dot | ||
251 | var gap = this.readByte() | ||
252 | if (gap != 0) cell.gap = gap | ||
253 | } else if (cellType === CELL_TYPE_SQUARE) { | ||
254 | cell.type = 'square' | ||
255 | cell.color = this.readColor() | ||
256 | } else if (cellType === CELL_TYPE_STAR) { | ||
257 | cell.type = 'star' | ||
258 | cell.color = this.readColor() | ||
259 | } else if (cellType === CELL_TYPE_NEGA) { | ||
260 | cell.type = 'nega' | ||
261 | cell.color = this.readColor() | ||
262 | } else if (cellType === CELL_TYPE_TRIANGLE) { | ||
263 | cell.type = 'triangle' | ||
264 | cell.color = this.readColor() | ||
265 | cell.count = this.readByte() | ||
266 | } else if (cellType === CELL_TYPE_POLY) { | ||
267 | cell.type = 'poly' | ||
268 | cell.color = this.readColor() | ||
269 | cell.polyshape = this.readLong() | ||
270 | } else if (cellType === CELL_TYPE_YLOP) { | ||
271 | cell.type = 'ylop' | ||
272 | cell.color = this.readColor() | ||
273 | cell.polyshape = this.readLong() | ||
274 | } else if (cellType == CELL_TYPE_NONCE) { | ||
275 | cell.type = 'nonce' | ||
276 | } | ||
277 | |||
278 | var startEnd = this.readByte() | ||
279 | if (startEnd & CELL_START) cell.start = true | ||
280 | if (startEnd & CELL_END_LEFT) cell.end = 'left' | ||
281 | if (startEnd & CELL_END_RIGHT) cell.end = 'right' | ||
282 | if (startEnd & CELL_END_TOP) cell.end = 'top' | ||
283 | if (startEnd & CELL_END_BOTTOM) cell.end = 'bottom' | ||
284 | |||
285 | return cell | ||
286 | } | ||
287 | |||
288 | |||
289 | writeCell(cell) { | ||
290 | if (cell == null) { | ||
291 | this.writeByte(CELL_TYPE_NULL) | ||
292 | return | ||
293 | } | ||
294 | |||
295 | // Write cell type, then cell data, then generic data. | ||
296 | // Note that cell type starts at 1, since 0 is the "null type". | ||
297 | if (cell.type == 'line') { | ||
298 | this.writeByte(CELL_TYPE_LINE) | ||
299 | this.writeByte(cell.line) | ||
300 | this.writeByte(cell.dot) | ||
301 | this.writeByte(cell.gap) | ||
302 | } else if (cell.type == 'square') { | ||
303 | this.writeByte(CELL_TYPE_SQUARE) | ||
304 | this.writeColor(cell.color) | ||
305 | } else if (cell.type == 'star') { | ||
306 | this.writeByte(CELL_TYPE_STAR) | ||
307 | this.writeColor(cell.color) | ||
308 | } else if (cell.type == 'nega') { | ||
309 | this.writeByte(CELL_TYPE_NEGA) | ||
310 | this.writeColor(cell.color) | ||
311 | } else if (cell.type == 'triangle') { | ||
312 | this.writeByte(CELL_TYPE_TRIANGLE) | ||
313 | this.writeColor(cell.color) | ||
314 | this.writeByte(cell.count) | ||
315 | } else if (cell.type == 'poly') { | ||
316 | this.writeByte(CELL_TYPE_POLY) | ||
317 | this.writeColor(cell.color) | ||
318 | this.writeLong(cell.polyshape) | ||
319 | } else if (cell.type == 'ylop') { | ||
320 | this.writeByte(CELL_TYPE_YLOP) | ||
321 | this.writeColor(cell.color) | ||
322 | this.writeLong(cell.polyshape) | ||
323 | } | ||
324 | |||
325 | var startEnd = 0 | ||
326 | if (cell.start === true) startEnd |= CELL_START | ||
327 | if (cell.end == 'left') startEnd |= CELL_END_LEFT | ||
328 | if (cell.end == 'right') startEnd |= CELL_END_RIGHT | ||
329 | if (cell.end == 'top') startEnd |= CELL_END_TOP | ||
330 | if (cell.end == 'bottom') startEnd |= CELL_END_BOTTOM | ||
331 | this.writeByte(startEnd) | ||
332 | } | ||
333 | } | ||
334 | |||
335 | var CELL_TYPE_NULL = 0 | ||
336 | var CELL_TYPE_LINE = 1 | ||
337 | var CELL_TYPE_SQUARE = 2 | ||
338 | var CELL_TYPE_STAR = 3 | ||
339 | var CELL_TYPE_NEGA = 4 | ||
340 | var CELL_TYPE_TRIANGLE = 5 | ||
341 | var CELL_TYPE_POLY = 6 | ||
342 | var CELL_TYPE_YLOP = 7 | ||
343 | var CELL_TYPE_NONCE = 8 | ||
344 | |||
345 | var CELL_START = 1 | ||
346 | var CELL_END_LEFT = 2 | ||
347 | var CELL_END_RIGHT = 4 | ||
348 | var CELL_END_TOP = 8 | ||
349 | var CELL_END_BOTTOM = 16 | ||
350 | |||
351 | var GENERIC_FLAG_AUTOSOLVED = 1 | ||
352 | var GENERIC_FLAG_SYMMETRICAL = 2 | ||
353 | var GENERIC_FLAG_SYMMETRY_X = 4 | ||
354 | var GENERIC_FLAG_SYMMETRY_Y = 8 | ||
355 | var GENERIC_FLAG_PILLAR = 16 | ||
356 | |||
357 | var SETTINGS_FLAG_NCN = 1 | ||
358 | var SETTINGS_FLAG_SZP = 2 | ||
359 | var SETTINGS_FLAG_PP = 4 | ||
360 | var SETTINGS_FLAG_FFE = 8 | ||
361 | var SETTINGS_FLAG_FS = 16 | ||
362 | var SETTINGS_FLAG_CM = 32 | ||
363 | var SETTINGS_FLAG_IS = 64 | ||
364 | |||
365 | }) | ||